pay 2.6.11 → 3.0.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 (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +35 -693
  3. data/app/controllers/pay/webhooks/braintree_controller.rb +10 -3
  4. data/app/controllers/pay/webhooks/paddle_controller.rb +7 -8
  5. data/app/controllers/pay/webhooks/stripe_controller.rb +6 -3
  6. data/app/jobs/pay/{email_sync_job.rb → customer_sync_job.rb} +3 -4
  7. data/app/models/pay/application_record.rb +1 -5
  8. data/app/models/pay/charge.rb +32 -17
  9. data/app/models/pay/customer.rb +87 -0
  10. data/app/models/pay/merchant.rb +19 -0
  11. data/app/models/pay/payment_method.rb +41 -0
  12. data/app/models/pay/subscription.rb +34 -30
  13. data/app/models/pay/webhook.rb +36 -0
  14. data/app/views/layouts/pay/application.html.erb +2 -3
  15. data/app/views/pay/payments/show.html.erb +109 -81
  16. data/app/views/pay/user_mailer/receipt.html.erb +2 -2
  17. data/app/views/pay/user_mailer/refund.html.erb +2 -2
  18. data/config/locales/en.yml +1 -1
  19. data/db/migrate/1_create_pay_tables.rb +72 -0
  20. data/lib/generators/active_record/billable_generator.rb +44 -0
  21. data/lib/generators/active_record/merchant_generator.rb +44 -0
  22. data/lib/generators/active_record/templates/billable_migration.rb +17 -0
  23. data/lib/generators/active_record/templates/merchant_migration.rb +12 -0
  24. data/lib/generators/pay/{pay_generator.rb → billable_generator.rb} +2 -3
  25. data/lib/generators/pay/merchant_generator.rb +17 -0
  26. data/lib/generators/pay/orm_helpers.rb +10 -6
  27. data/lib/pay/adapter.rb +22 -0
  28. data/lib/pay/attributes.rb +74 -0
  29. data/lib/pay/billable/sync_customer.rb +30 -0
  30. data/lib/pay/braintree/billable.rb +130 -105
  31. data/lib/pay/braintree/payment_method.rb +33 -0
  32. data/lib/pay/braintree/subscription.rb +9 -12
  33. data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -1
  34. data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +4 -4
  35. data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
  36. data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -1
  37. data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +2 -2
  38. data/lib/pay/braintree/webhooks/subscription_went_active.rb +1 -1
  39. data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +1 -1
  40. data/lib/pay/braintree.rb +3 -2
  41. data/lib/pay/engine.rb +6 -1
  42. data/lib/pay/env.rb +8 -0
  43. data/lib/pay/fake_processor/billable.rb +45 -21
  44. data/lib/pay/fake_processor/payment_method.rb +21 -0
  45. data/lib/pay/fake_processor/subscription.rb +11 -8
  46. data/lib/pay/fake_processor.rb +2 -1
  47. data/lib/pay/merchant.rb +37 -0
  48. data/lib/pay/nano_id.rb +13 -0
  49. data/lib/pay/paddle/billable.rb +18 -48
  50. data/lib/pay/paddle/charge.rb +5 -5
  51. data/lib/pay/paddle/payment_method.rb +58 -0
  52. data/lib/pay/paddle/response.rb +0 -0
  53. data/lib/pay/paddle/subscription.rb +49 -8
  54. data/lib/pay/paddle/webhooks/subscription_cancelled.rb +6 -3
  55. data/lib/pay/paddle/webhooks/subscription_created.rb +1 -40
  56. data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
  57. data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +23 -23
  58. data/lib/pay/paddle/webhooks/subscription_updated.rb +2 -2
  59. data/lib/pay/paddle.rb +7 -3
  60. data/lib/pay/payment.rb +1 -1
  61. data/lib/pay/receipts.rb +35 -7
  62. data/lib/pay/stripe/billable.rb +82 -93
  63. data/lib/pay/stripe/charge.rb +65 -4
  64. data/lib/pay/stripe/merchant.rb +66 -0
  65. data/lib/pay/stripe/payment_method.rb +61 -0
  66. data/lib/pay/stripe/subscription.rb +91 -24
  67. data/lib/pay/stripe/webhooks/account_updated.rb +16 -0
  68. data/lib/pay/stripe/webhooks/charge_refunded.rb +2 -7
  69. data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -8
  70. data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +15 -0
  71. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +15 -0
  72. data/lib/pay/stripe/webhooks/customer_deleted.rb +7 -15
  73. data/lib/pay/stripe/webhooks/customer_updated.rb +10 -3
  74. data/lib/pay/stripe/webhooks/payment_action_required.rb +2 -2
  75. data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +6 -14
  76. data/lib/pay/stripe/webhooks/payment_method_attached.rb +15 -0
  77. data/lib/pay/stripe/webhooks/payment_method_detached.rb +12 -0
  78. data/lib/pay/stripe/webhooks/payment_method_updated.rb +10 -4
  79. data/lib/pay/stripe/webhooks/subscription_created.rb +1 -35
  80. data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -9
  81. data/lib/pay/stripe/webhooks/subscription_renewing.rb +14 -6
  82. data/lib/pay/stripe/webhooks/subscription_updated.rb +1 -28
  83. data/lib/pay/stripe.rb +17 -3
  84. data/lib/pay/version.rb +1 -1
  85. data/lib/pay/webhooks/delegator.rb +4 -0
  86. data/lib/pay/webhooks/process_job.rb +9 -0
  87. data/lib/pay/webhooks.rb +1 -0
  88. data/lib/pay.rb +8 -57
  89. metadata +34 -36
  90. data/db/migrate/20170205020145_create_pay_subscriptions.rb +0 -17
  91. data/db/migrate/20170727235816_create_pay_charges.rb +0 -18
  92. data/db/migrate/20190816015720_add_status_to_pay_subscriptions.rb +0 -14
  93. data/db/migrate/20200603134434_add_data_to_pay_models.rb +0 -22
  94. data/db/migrate/20210423235138_add_currency_to_pay_charges.rb +0 -5
  95. data/lib/generators/active_record/pay_generator.rb +0 -58
  96. data/lib/generators/active_record/templates/migration.rb +0 -9
  97. data/lib/pay/billable/sync_email.rb +0 -40
  98. data/lib/pay/billable.rb +0 -168
@@ -6,7 +6,7 @@ module Pay
6
6
  end
7
7
 
8
8
  def create
9
- delegate_event(verified_event)
9
+ queue_event(verified_event)
10
10
  head :ok
11
11
  rescue ::Braintree::InvalidSignature
12
12
  head :bad_request
@@ -14,8 +14,15 @@ module Pay
14
14
 
15
15
  private
16
16
 
17
- def delegate_event(event)
18
- Pay::Webhooks.instrument type: "braintree.#{event.kind}", event: event
17
+ def queue_event(event)
18
+ return unless Pay::Webhooks.delegator.listening?("braintree.#{event.kind}")
19
+
20
+ record = Pay::Webhook.create(
21
+ processor: :braintree,
22
+ event_type: event.kind,
23
+ event: {bt_signature: params[:bt_signature], bt_payload: params[:bt_payload]}
24
+ )
25
+ Pay::Webhooks::ProcessJob.perform_later(record)
19
26
  end
20
27
 
21
28
  def verified_event
@@ -6,7 +6,7 @@ module Pay
6
6
  end
7
7
 
8
8
  def create
9
- delegate_event(verified_event)
9
+ queue_event(verified_event)
10
10
  head :ok
11
11
  rescue Pay::Paddle::Error
12
12
  head :bad_request
@@ -14,22 +14,21 @@ module Pay
14
14
 
15
15
  private
16
16
 
17
- def delegate_event(event)
18
- Pay::Webhooks.instrument type: "paddle.#{type}", event: event
19
- end
17
+ def queue_event(event)
18
+ return unless Pay::Webhooks.delegator.listening?("paddle.#{params[:alert_name]}")
20
19
 
21
- def type
22
- params[:alert_name]
20
+ record = Pay::Webhook.create(processor: :paddle, event_type: params[:alert_name], event: event)
21
+ Pay::Webhooks::ProcessJob.perform_later(record)
23
22
  end
24
23
 
25
24
  def verified_event
26
- event = check_params.as_json
25
+ event = verify_params.as_json
27
26
  verifier = Pay::Paddle::Webhooks::SignatureVerifier.new(event)
28
27
  return event if verifier.verify
29
28
  raise Pay::Paddle::Error, "Unable to verify Paddle webhook event"
30
29
  end
31
30
 
32
- def check_params
31
+ def verify_params
33
32
  params.except(:action, :controller).permit!
34
33
  end
35
34
  end
@@ -6,7 +6,7 @@ module Pay
6
6
  end
7
7
 
8
8
  def create
9
- delegate_event(verified_event)
9
+ queue_event(verified_event)
10
10
  head :ok
11
11
  rescue ::Stripe::SignatureVerificationError => e
12
12
  log_error(e)
@@ -15,8 +15,11 @@ module Pay
15
15
 
16
16
  private
17
17
 
18
- def delegate_event(event)
19
- Pay::Webhooks.instrument type: "stripe.#{event.type}", event: event
18
+ def queue_event(event)
19
+ return unless Pay::Webhooks.delegator.listening?("stripe.#{event.type}")
20
+
21
+ record = Pay::Webhook.create(processor: :stripe, event_type: event.type, event: event)
22
+ Pay::Webhooks::ProcessJob.perform_later(record)
20
23
  end
21
24
 
22
25
  def verified_event
@@ -1,10 +1,9 @@
1
1
  module Pay
2
- class EmailSyncJob < ApplicationJob
2
+ class CustomerSyncJob < ApplicationJob
3
3
  queue_as :default
4
4
 
5
- def perform(id, class_name)
6
- billable = class_name.constantize.find(id)
7
- billable.sync_email_with_processor
5
+ def perform(pay_customer_id)
6
+ Pay::Customer.find(pay_customer_id).update_customer!
8
7
  rescue ActiveRecord::RecordNotFound
9
8
  Rails.logger.info "Couldn't find a #{class_name} with ID = #{id}"
10
9
  end
@@ -1,10 +1,6 @@
1
1
  module Pay
2
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
+ self.table_name_prefix = "pay_"
9
5
  end
10
6
  end
@@ -1,42 +1,56 @@
1
1
  module Pay
2
2
  class Charge < Pay::ApplicationRecord
3
- self.table_name = Pay.chargeable_table
4
-
5
- # Only serialize for non-json columns
6
- serialize :data unless json_column?("data")
3
+ self.inheritance_column = nil
7
4
 
8
5
  # Associations
9
- belongs_to :owner, polymorphic: true
6
+ belongs_to :customer
7
+ belongs_to :subscription, optional: true
10
8
 
11
9
  # Scopes
12
10
  scope :sorted, -> { order(created_at: :desc) }
11
+ scope :with_active_customer, -> { joins(:customer).merge(Customer.active) }
12
+ scope :with_deleted_customer, -> { joins(:customer).merge(Customer.deleted) }
13
13
  default_scope -> { sorted }
14
14
 
15
15
  # Validations
16
16
  validates :amount, presence: true
17
- validates :processor, presence: true
18
- validates :processor_id, presence: true
19
- validates :card_type, presence: true
17
+ validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
20
18
 
19
+ # Store the payment method kind (card, paypal, etc)
21
20
  store_accessor :data, :paddle_receipt_url
21
+ store_accessor :data, :stripe_account
22
+
23
+ # Payment method attributes
24
+ store_accessor :data, :payment_method_type # card, paypal, sepa, etc
25
+ store_accessor :data, :brand # Visa, Mastercard, Discover, PayPal
26
+ store_accessor :data, :last4
27
+ store_accessor :data, :exp_month
28
+ store_accessor :data, :exp_year
29
+ store_accessor :data, :email # PayPal email, etc
30
+ store_accessor :data, :username # Venmo
31
+ store_accessor :data, :bank
22
32
 
23
33
  # Helpers for payment processors
24
34
  %w[braintree stripe paddle fake_processor].each do |processor_name|
25
35
  define_method "#{processor_name}?" do
26
- processor == processor_name
36
+ customer.processor == processor_name
27
37
  end
28
38
 
29
39
  scope processor_name, -> { where(processor: processor_name) }
30
40
  end
31
41
 
32
- def payment_processor
33
- @payment_processor ||= payment_processor_for(processor).new(self)
42
+ def self.find_by_processor_and_id(processor, processor_id)
43
+ joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
34
44
  end
35
45
 
36
- def payment_processor_for(name)
46
+ def self.pay_processor_for(name)
37
47
  "Pay::#{name.to_s.classify}::Charge".constantize
38
48
  end
39
49
 
50
+ def payment_processor
51
+ @payment_processor ||= self.class.pay_processor_for(customer.processor).new(self)
52
+ end
53
+
40
54
  def processor_charge
41
55
  payment_processor.charge
42
56
  end
@@ -47,11 +61,12 @@ module Pay
47
61
  end
48
62
 
49
63
  def charged_to
50
- "#{card_type} (**** **** **** #{card_last4})"
51
- end
52
-
53
- def paypal?
54
- braintree? && card_type == "PayPal"
64
+ case payment_method_type
65
+ when "card"
66
+ "#{brand} (**** **** **** #{last4})"
67
+ when "paypal"
68
+ "#{brand} (#{email})"
69
+ end
55
70
  end
56
71
  end
57
72
  end
@@ -0,0 +1,87 @@
1
+ module Pay
2
+ class Customer < Pay::ApplicationRecord
3
+ belongs_to :owner, polymorphic: true
4
+ has_many :charges, dependent: :destroy
5
+ has_many :subscriptions, dependent: :destroy
6
+ has_many :payment_methods, dependent: :destroy
7
+ has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::PaymentMethod"
8
+
9
+ scope :active, -> { where(deleted_at: nil) }
10
+ scope :deleted, -> { where.not(deleted_at: nil) }
11
+
12
+ validates :processor, presence: true
13
+ validates :processor_id, allow_blank: true, uniqueness: {scope: :processor, case_sensitive: true}
14
+
15
+ attribute :plan, :string
16
+ attribute :quantity, :integer
17
+ attribute :payment_method_token, :string
18
+
19
+ # Account(s) for marketplace payments
20
+ store_accessor :data, :stripe_account
21
+ store_accessor :data, :braintree_account
22
+
23
+ delegate :email, to: :owner
24
+ delegate_missing_to :pay_processor
25
+
26
+ def self.processor_for(name)
27
+ "Pay::#{name.to_s.classify}::Billable".constantize
28
+ end
29
+
30
+ def pay_processor
31
+ @pay_processor ||= self.class.processor_for(processor).new(self)
32
+ end
33
+
34
+ def update_payment_method(payment_method_id)
35
+ add_payment_method(payment_method_id, default: true)
36
+ end
37
+
38
+ def subscription(name: Pay.default_product_name)
39
+ subscriptions.loaded? ? subscriptions.reverse.detect { |s| s.name == name } : subscriptions.for_name(name).last
40
+ end
41
+
42
+ def subscribed?(name: Pay.default_product_name, processor_plan: nil)
43
+ subscription = subscription(name: name)
44
+
45
+ return false if subscription.nil?
46
+ return subscription.active? if processor_plan.nil?
47
+
48
+ subscription.active? && subscription.processor_plan == processor_plan
49
+ end
50
+
51
+ def on_trial?(name: Pay.default_product_name, plan: nil)
52
+ sub = subscription(name: name)
53
+ return sub&.on_trial? if plan.nil?
54
+
55
+ sub&.on_trial? && sub.processor_plan == plan
56
+ end
57
+
58
+ def on_trial_or_subscribed?(name: Pay.default_product_name, processor_plan: nil)
59
+ on_trial?(name: name, plan: processor_plan) ||
60
+ subscribed?(name: name, processor_plan: processor_plan)
61
+ end
62
+
63
+ def has_incomplete_payment?
64
+ subscriptions.active.incomplete.any?
65
+ end
66
+
67
+ def customer_name
68
+ [owner.try(:first_name), owner.try(:last_name)].compact.join(" ")
69
+ end
70
+
71
+ def active?
72
+ deleted_at.nil?
73
+ end
74
+
75
+ def deleted?
76
+ deleted_at.present?
77
+ end
78
+
79
+ %w[stripe braintree paddle fake_processor].each do |processor_name|
80
+ scope processor_name, -> { where(processor: processor_name) }
81
+
82
+ define_method "#{processor_name}?" do
83
+ processor == processor_name
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,19 @@
1
+ module Pay
2
+ class Merchant < Pay::ApplicationRecord
3
+ belongs_to :owner, polymorphic: true
4
+
5
+ validates :processor, presence: true
6
+
7
+ store_accessor :data, :onboarding_complete
8
+
9
+ delegate_missing_to :pay_processor
10
+
11
+ def self.processor_for(name)
12
+ "Pay::#{name.to_s.classify}::Merchant".constantize
13
+ end
14
+
15
+ def pay_processor
16
+ @pay_processor ||= self.class.processor_for(processor).new(self)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ module Pay
2
+ class PaymentMethod < Pay::ApplicationRecord
3
+ self.inheritance_column = nil
4
+
5
+ belongs_to :customer
6
+
7
+ store_accessor :data, :brand # Visa, Mastercard, Discover, PayPal
8
+ store_accessor :data, :last4
9
+ store_accessor :data, :exp_month
10
+ store_accessor :data, :exp_year
11
+ store_accessor :data, :email # PayPal email, etc
12
+ store_accessor :data, :username
13
+ store_accessor :data, :bank
14
+
15
+ # Aliases to share PaymentMethodAttributes
16
+ alias_attribute :payment_method_type, :type
17
+
18
+ validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
19
+
20
+ def self.find_by_processor_and_id(processor, processor_id)
21
+ joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
22
+ end
23
+
24
+ def self.pay_processor_for(name)
25
+ "Pay::#{name.to_s.classify}::PaymentMethod".constantize
26
+ end
27
+
28
+ def payment_processor
29
+ @payment_processor ||= self.class.pay_processor_for(customer.processor).new(self)
30
+ end
31
+
32
+ def make_default!
33
+ return if default?
34
+
35
+ payment_processor.make_default!
36
+
37
+ customer.payment_methods.update_all(default: false)
38
+ update!(default: true)
39
+ end
40
+ end
41
+ end
@@ -1,22 +1,10 @@
1
1
  module Pay
2
2
  class Subscription < Pay::ApplicationRecord
3
- self.table_name = Pay.subscription_table
4
-
5
3
  STATUSES = %w[incomplete incomplete_expired trialing active past_due canceled unpaid paused]
6
4
 
7
- # Only serialize for non-json columns
8
- serialize :data unless json_column?("data")
9
-
10
5
  # Associations
11
- belongs_to :owner, polymorphic: true
12
-
13
- # Validations
14
- validates :name, presence: true
15
- validates :processor, presence: true
16
- validates :processor_id, presence: true
17
- validates :processor_plan, presence: true
18
- validates :quantity, presence: true
19
- validates :status, presence: true
6
+ belongs_to :customer
7
+ has_many :charges, class_name: "Pay::Charge", foreign_key: :pay_subscription_id
20
8
 
21
9
  # Scopes
22
10
  scope :for_name, ->(name) { where(name: name) }
@@ -26,37 +14,53 @@ module Pay
26
14
  scope :active, -> { where(ends_at: nil).or(on_grace_period).or(on_trial) }
27
15
  scope :incomplete, -> { where(status: :incomplete) }
28
16
  scope :past_due, -> { where(status: :past_due) }
17
+ scope :with_active_customer, -> { joins(:customer).merge(Customer.active) }
18
+ scope :with_deleted_customer, -> { joins(:customer).merge(Customer.deleted) }
19
+
20
+ # Callbacks
21
+ before_destroy :cancel_now!, if: :active?
29
22
 
30
- # TODO: Include these with a module
31
23
  store_accessor :data, :paddle_update_url
32
24
  store_accessor :data, :paddle_cancel_url
33
25
  store_accessor :data, :paddle_paused_from
26
+ store_accessor :data, :stripe_account
34
27
 
35
28
  attribute :prorate, :boolean, default: true
36
29
 
37
- # Helpers for payment processors
30
+ # Validations
31
+ validates :name, presence: true
32
+ validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
33
+ validates :processor_plan, presence: true
34
+ validates :quantity, presence: true, numericality: {only_integer: true, greater_than_or_equal_to: 1}
35
+ validates :status, presence: true
36
+
37
+ delegate :on_grace_period?,
38
+ :paused?,
39
+ :pause,
40
+ :cancel,
41
+ :cancel_now!,
42
+ to: :payment_processor
43
+
44
+ # Helper methods for payment processors
38
45
  %w[braintree stripe paddle fake_processor].each do |processor_name|
39
46
  define_method "#{processor_name}?" do
40
- processor == processor_name
47
+ customer.processor == processor_name
41
48
  end
42
49
 
43
- scope processor_name, -> { where(processor: processor_name) }
50
+ scope processor_name, -> { joins(:customer).where(pay_customers: {processor: processor_name}) }
44
51
  end
45
52
 
46
- def payment_processor
47
- @payment_processor ||= payment_processor_for(processor).new(self)
53
+ def self.find_by_processor_and_id(processor, processor_id)
54
+ joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
48
55
  end
49
56
 
50
- def payment_processor_for(name)
57
+ def self.pay_processor_for(name)
51
58
  "Pay::#{name.to_s.classify}::Subscription".constantize
52
59
  end
53
60
 
54
- delegate :on_grace_period?,
55
- :paused?,
56
- :pause,
57
- :cancel,
58
- :cancel_now!,
59
- to: :payment_processor
61
+ def payment_processor
62
+ @payment_processor ||= self.class.pay_processor_for(customer.processor).new(self)
63
+ end
60
64
 
61
65
  def no_prorate
62
66
  self.prorate = false
@@ -101,13 +105,14 @@ module Pay
101
105
 
102
106
  def resume
103
107
  payment_processor.resume
104
- update(ends_at: nil, status: "active")
108
+ update(ends_at: nil, status: :active)
105
109
  self
106
110
  end
107
111
 
108
112
  def swap(plan)
113
+ raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
109
114
  payment_processor.swap(plan)
110
- update(processor_plan: plan, ends_at: nil)
115
+ update(processor_plan: plan, ends_at: nil, status: :active)
111
116
  end
112
117
 
113
118
  def swap_and_invoice(plan)
@@ -120,7 +125,6 @@ module Pay
120
125
  end
121
126
 
122
127
  def latest_payment
123
- return unless stripe?
124
128
  processor_subscription(expand: ["latest_invoice.payment_intent"]).latest_invoice.payment_intent
125
129
  end
126
130
  end