pay 2.7.1 → 3.0.2
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.
- checksums.yaml +4 -4
- data/README.md +34 -715
- data/app/controllers/pay/webhooks/braintree_controller.rb +10 -3
- data/app/controllers/pay/webhooks/paddle_controller.rb +7 -8
- data/app/controllers/pay/webhooks/stripe_controller.rb +6 -3
- data/app/jobs/pay/{email_sync_job.rb → customer_sync_job.rb} +3 -4
- data/app/models/pay/application_record.rb +1 -5
- data/app/models/pay/charge.rb +54 -17
- data/app/models/pay/customer.rb +87 -0
- data/app/models/pay/merchant.rb +19 -0
- data/app/models/pay/payment_method.rb +41 -0
- data/app/models/pay/subscription.rb +42 -30
- data/app/models/pay/webhook.rb +36 -0
- data/app/views/layouts/pay/application.html.erb +2 -3
- data/app/views/pay/payments/show.html.erb +108 -81
- data/app/views/pay/user_mailer/receipt.html.erb +2 -2
- data/app/views/pay/user_mailer/refund.html.erb +2 -2
- data/config/locales/en.yml +1 -1
- data/db/migrate/1_create_pay_tables.rb +72 -0
- data/lib/generators/active_record/templates/billable_migration.rb +1 -0
- data/lib/pay/attributes.rb +74 -0
- data/lib/pay/billable/sync_customer.rb +30 -0
- data/lib/pay/braintree/billable.rb +133 -110
- data/lib/pay/braintree/payment_method.rb +42 -0
- data/lib/pay/braintree/subscription.rb +9 -12
- data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_charged_successfully.rb +4 -4
- data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +2 -2
- data/lib/pay/braintree/webhooks/subscription_went_active.rb +1 -1
- data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +1 -1
- data/lib/pay/braintree.rb +3 -2
- data/lib/pay/engine.rb +6 -1
- data/lib/pay/fake_processor/billable.rb +45 -21
- data/lib/pay/fake_processor/payment_method.rb +21 -0
- data/lib/pay/fake_processor/subscription.rb +11 -8
- data/lib/pay/fake_processor.rb +2 -1
- data/lib/pay/nano_id.rb +13 -0
- data/lib/pay/paddle/billable.rb +18 -48
- data/lib/pay/paddle/charge.rb +5 -5
- data/lib/pay/paddle/payment_method.rb +60 -0
- data/lib/pay/paddle/response.rb +0 -0
- data/lib/pay/paddle/subscription.rb +49 -8
- data/lib/pay/paddle/webhooks/subscription_cancelled.rb +6 -3
- data/lib/pay/paddle/webhooks/subscription_created.rb +1 -40
- data/lib/pay/paddle/webhooks/subscription_payment_refunded.rb +3 -3
- data/lib/pay/paddle/webhooks/subscription_payment_succeeded.rb +26 -28
- data/lib/pay/paddle/webhooks/subscription_updated.rb +2 -2
- data/lib/pay/paddle.rb +7 -3
- data/lib/pay/payment.rb +1 -1
- data/lib/pay/receipts.rb +35 -7
- data/lib/pay/stripe/billable.rb +75 -76
- data/lib/pay/stripe/charge.rb +44 -17
- data/lib/pay/stripe/merchant.rb +10 -10
- data/lib/pay/stripe/payment_method.rb +61 -0
- data/lib/pay/stripe/subscription.rb +55 -22
- data/lib/pay/stripe/webhooks/account_updated.rb +2 -3
- data/lib/pay/stripe/webhooks/charge_refunded.rb +1 -1
- data/lib/pay/stripe/webhooks/charge_succeeded.rb +2 -2
- data/lib/pay/stripe/webhooks/checkout_session_async_payment_succeeded.rb +3 -1
- data/lib/pay/stripe/webhooks/checkout_session_completed.rb +3 -1
- data/lib/pay/stripe/webhooks/customer_deleted.rb +7 -15
- data/lib/pay/stripe/webhooks/customer_updated.rb +10 -3
- data/lib/pay/stripe/webhooks/payment_action_required.rb +2 -2
- data/lib/pay/stripe/webhooks/payment_intent_succeeded.rb +6 -8
- data/lib/pay/stripe/webhooks/payment_method_attached.rb +15 -0
- data/lib/pay/stripe/webhooks/payment_method_detached.rb +12 -0
- data/lib/pay/stripe/webhooks/payment_method_updated.rb +10 -4
- data/lib/pay/stripe/webhooks/subscription_created.rb +1 -1
- data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -1
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +12 -2
- data/lib/pay/stripe.rb +6 -3
- data/lib/pay/version.rb +1 -1
- data/lib/pay/webhooks/delegator.rb +4 -0
- data/lib/pay/webhooks/process_job.rb +9 -0
- data/lib/pay/webhooks.rb +1 -0
- data/lib/pay.rb +7 -78
- data/lib/tasks/pay.rake +20 -0
- metadata +23 -36
- data/app/models/pay.rb +0 -5
- data/db/migrate/20170205020145_create_pay_subscriptions.rb +0 -17
- data/db/migrate/20170727235816_create_pay_charges.rb +0 -18
- data/db/migrate/20190816015720_add_status_to_pay_subscriptions.rb +0 -14
- data/db/migrate/20200603134434_add_data_to_pay_models.rb +0 -6
- data/db/migrate/20210309004259_add_data_to_pay_billable.rb +0 -10
- data/db/migrate/20210406215234_add_currency_to_pay_charges.rb +0 -5
- data/db/migrate/20210406215506_add_application_fee_to_pay_models.rb +0 -7
- data/lib/pay/billable/sync_email.rb +0 -40
- data/lib/pay/billable.rb +0 -172
@@ -6,7 +6,7 @@ module Pay
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def create
|
9
|
-
|
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
|
18
|
-
Pay::Webhooks.
|
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
|
-
|
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
|
18
|
-
Pay::Webhooks.
|
19
|
-
end
|
17
|
+
def queue_event(event)
|
18
|
+
return unless Pay::Webhooks.delegator.listening?("paddle.#{params[:alert_name]}")
|
20
19
|
|
21
|
-
|
22
|
-
|
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 =
|
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
|
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
|
-
|
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
|
19
|
-
Pay::Webhooks.
|
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
|
2
|
+
class CustomerSyncJob < ApplicationJob
|
3
3
|
queue_as :default
|
4
4
|
|
5
|
-
def perform(
|
6
|
-
|
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
|
data/app/models/pay/charge.rb
CHANGED
@@ -1,44 +1,56 @@
|
|
1
1
|
module Pay
|
2
2
|
class Charge < Pay::ApplicationRecord
|
3
|
-
self.
|
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 :
|
10
|
-
belongs_to :subscription, optional: true
|
6
|
+
belongs_to :customer
|
7
|
+
belongs_to :subscription, optional: true
|
11
8
|
|
12
9
|
# Scopes
|
13
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) }
|
14
13
|
default_scope -> { sorted }
|
15
14
|
|
16
15
|
# Validations
|
17
16
|
validates :amount, presence: true
|
18
|
-
validates :
|
19
|
-
validates :processor_id, presence: true
|
20
|
-
validates :card_type, presence: true
|
17
|
+
validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
|
21
18
|
|
19
|
+
# Store the payment method kind (card, paypal, etc)
|
22
20
|
store_accessor :data, :paddle_receipt_url
|
23
21
|
store_accessor :data, :stripe_account
|
24
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
|
32
|
+
|
25
33
|
# Helpers for payment processors
|
26
34
|
%w[braintree stripe paddle fake_processor].each do |processor_name|
|
27
35
|
define_method "#{processor_name}?" do
|
28
|
-
processor == processor_name
|
36
|
+
customer.processor == processor_name
|
29
37
|
end
|
30
38
|
|
31
39
|
scope processor_name, -> { where(processor: processor_name) }
|
32
40
|
end
|
33
41
|
|
34
|
-
def
|
35
|
-
|
42
|
+
def self.find_by_processor_and_id(processor, processor_id)
|
43
|
+
joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
|
36
44
|
end
|
37
45
|
|
38
|
-
def
|
46
|
+
def self.pay_processor_for(name)
|
39
47
|
"Pay::#{name.to_s.classify}::Charge".constantize
|
40
48
|
end
|
41
49
|
|
50
|
+
def payment_processor
|
51
|
+
@payment_processor ||= self.class.pay_processor_for(customer.processor).new(self)
|
52
|
+
end
|
53
|
+
|
42
54
|
def processor_charge
|
43
55
|
payment_processor.charge
|
44
56
|
end
|
@@ -49,11 +61,36 @@ module Pay
|
|
49
61
|
end
|
50
62
|
|
51
63
|
def charged_to
|
52
|
-
|
53
|
-
|
64
|
+
case payment_method_type
|
65
|
+
when "card"
|
66
|
+
"#{brand.titleize} (**** **** **** #{last4})"
|
67
|
+
when "paypal"
|
68
|
+
"#{brand} (#{email})"
|
54
69
|
|
55
|
-
|
56
|
-
|
70
|
+
# Braintree
|
71
|
+
when "venmo"
|
72
|
+
"#{brand.titleize} #{username}"
|
73
|
+
when "us_bank_account"
|
74
|
+
"#{bank} #{last4}"
|
75
|
+
|
76
|
+
# Stripe
|
77
|
+
when "acss_debit"
|
78
|
+
"#{bank} #{last4}"
|
79
|
+
when "eps", "fpx", "ideal", "p24"
|
80
|
+
bank
|
81
|
+
|
82
|
+
when "au_becs_debit"
|
83
|
+
"BECS Debit #{last4}"
|
84
|
+
|
85
|
+
when "bacs_debit"
|
86
|
+
"Bacs Debit #{last4}"
|
87
|
+
|
88
|
+
when "sepa_debit"
|
89
|
+
"SEPA Debit #{last4}"
|
90
|
+
|
91
|
+
else
|
92
|
+
payment_method_type&.titleize
|
93
|
+
end
|
57
94
|
end
|
58
95
|
end
|
59
96
|
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,24 +1,11 @@
|
|
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 :
|
6
|
+
belongs_to :customer
|
12
7
|
has_many :charges, class_name: "Pay::Charge", foreign_key: :pay_subscription_id
|
13
8
|
|
14
|
-
# Validations
|
15
|
-
validates :name, presence: true
|
16
|
-
validates :processor, presence: true
|
17
|
-
validates :processor_id, presence: true
|
18
|
-
validates :processor_plan, presence: true
|
19
|
-
validates :quantity, presence: true
|
20
|
-
validates :status, presence: true
|
21
|
-
|
22
9
|
# Scopes
|
23
10
|
scope :for_name, ->(name) { where(name: name) }
|
24
11
|
scope :on_trial, -> { where.not(trial_ends_at: nil).where("#{table_name}.trial_ends_at > ?", Time.zone.now) }
|
@@ -27,8 +14,12 @@ module Pay
|
|
27
14
|
scope :active, -> { where(ends_at: nil).or(on_grace_period).or(on_trial) }
|
28
15
|
scope :incomplete, -> { where(status: :incomplete) }
|
29
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_if_active
|
30
22
|
|
31
|
-
# TODO: Include these with a module
|
32
23
|
store_accessor :data, :paddle_update_url
|
33
24
|
store_accessor :data, :paddle_cancel_url
|
34
25
|
store_accessor :data, :paddle_paused_from
|
@@ -36,29 +27,40 @@ module Pay
|
|
36
27
|
|
37
28
|
attribute :prorate, :boolean, default: true
|
38
29
|
|
39
|
-
#
|
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
|
40
45
|
%w[braintree stripe paddle fake_processor].each do |processor_name|
|
41
46
|
define_method "#{processor_name}?" do
|
42
|
-
processor == processor_name
|
47
|
+
customer.processor == processor_name
|
43
48
|
end
|
44
49
|
|
45
|
-
scope processor_name, -> { where(processor: processor_name) }
|
50
|
+
scope processor_name, -> { joins(:customer).where(pay_customers: {processor: processor_name}) }
|
46
51
|
end
|
47
52
|
|
48
|
-
def
|
49
|
-
|
53
|
+
def self.find_by_processor_and_id(processor, processor_id)
|
54
|
+
joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
|
50
55
|
end
|
51
56
|
|
52
|
-
def
|
57
|
+
def self.pay_processor_for(name)
|
53
58
|
"Pay::#{name.to_s.classify}::Subscription".constantize
|
54
59
|
end
|
55
60
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
:cancel,
|
60
|
-
:cancel_now!,
|
61
|
-
to: :payment_processor
|
61
|
+
def payment_processor
|
62
|
+
@payment_processor ||= self.class.pay_processor_for(customer.processor).new(self)
|
63
|
+
end
|
62
64
|
|
63
65
|
def no_prorate
|
64
66
|
self.prorate = false
|
@@ -103,13 +105,14 @@ module Pay
|
|
103
105
|
|
104
106
|
def resume
|
105
107
|
payment_processor.resume
|
106
|
-
update(ends_at: nil, status:
|
108
|
+
update(ends_at: nil, status: :active)
|
107
109
|
self
|
108
110
|
end
|
109
111
|
|
110
112
|
def swap(plan)
|
113
|
+
raise ArgumentError, "plan must be a string. Got `#{plan.inspect}` instead." unless plan.is_a?(String)
|
111
114
|
payment_processor.swap(plan)
|
112
|
-
update(processor_plan: plan, ends_at: nil)
|
115
|
+
update(processor_plan: plan, ends_at: nil, status: :active)
|
113
116
|
end
|
114
117
|
|
115
118
|
def swap_and_invoice(plan)
|
@@ -122,8 +125,17 @@ module Pay
|
|
122
125
|
end
|
123
126
|
|
124
127
|
def latest_payment
|
125
|
-
return unless stripe?
|
126
128
|
processor_subscription(expand: ["latest_invoice.payment_intent"]).latest_invoice.payment_intent
|
127
129
|
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def cancel_if_active
|
134
|
+
if active?
|
135
|
+
cancel_now!
|
136
|
+
end
|
137
|
+
rescue => e
|
138
|
+
Rails.logger.info "[Pay] Unable to automatically cancel subscription `#{processor} #{id}`: #{e.message}"
|
139
|
+
end
|
128
140
|
end
|
129
141
|
end
|