pay 7.3.0 → 11.2.2
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.
- checksums.yaml +4 -4
- data/README.md +8 -4
- data/app/controllers/pay/payments_controller.rb +2 -0
- data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +45 -0
- data/app/jobs/pay/customer_sync_job.rb +1 -1
- data/app/models/concerns/pay/routing.rb +13 -0
- data/{lib → app/models}/pay/braintree/charge.rb +7 -12
- data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +33 -71
- data/{lib → app/models}/pay/braintree/payment_method.rb +4 -10
- data/{lib → app/models}/pay/braintree/subscription.rb +23 -61
- data/app/models/pay/charge.rb +16 -45
- data/app/models/pay/customer.rb +5 -16
- data/app/models/pay/fake_processor/charge.rb +19 -0
- data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +28 -38
- data/{lib → app/models}/pay/fake_processor/merchant.rb +4 -9
- data/app/models/pay/fake_processor/payment_method.rb +13 -0
- data/app/models/pay/fake_processor/subscription.rb +70 -0
- data/app/models/pay/lemon_squeezy/charge.rb +96 -0
- data/app/models/pay/lemon_squeezy/customer.rb +80 -0
- data/app/models/pay/lemon_squeezy/payment_method.rb +29 -0
- data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
- data/app/models/pay/merchant.rb +2 -11
- data/{lib → app/models}/pay/paddle_billing/charge.rb +15 -13
- data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +20 -35
- data/{lib → app/models}/pay/paddle_billing/payment_method.rb +13 -13
- data/{lib → app/models}/pay/paddle_billing/subscription.rb +40 -43
- data/{lib → app/models}/pay/paddle_classic/charge.rb +15 -18
- data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +11 -31
- data/{lib → app/models}/pay/paddle_classic/payment_method.rb +3 -11
- data/{lib → app/models}/pay/paddle_classic/subscription.rb +17 -37
- data/app/models/pay/payment_method.rb +4 -5
- data/app/models/pay/stripe/charge.rb +155 -0
- data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +78 -111
- data/{lib → app/models}/pay/stripe/merchant.rb +5 -20
- data/{lib → app/models}/pay/stripe/payment_method.rb +11 -17
- data/{lib → app/models}/pay/stripe/subscription.rb +83 -112
- data/app/models/pay/subscription.rb +13 -47
- data/app/models/pay/webhook.rb +5 -1
- data/app/views/pay/user_mailer/payment_action_required.text.erb +9 -0
- data/app/views/pay/user_mailer/payment_failed.text.erb +9 -0
- data/app/views/pay/user_mailer/receipt.text.erb +20 -0
- data/app/views/pay/user_mailer/refund.text.erb +21 -0
- data/app/views/pay/user_mailer/subscription_renewing.text.erb +8 -0
- data/app/views/pay/user_mailer/subscription_trial_ended.text.erb +8 -0
- data/app/views/pay/user_mailer/subscription_trial_will_end.text.erb +8 -0
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20250415151129_add_object_to_pay_models.rb +7 -0
- data/db/migrate/2_add_pay_sti_columns.rb +24 -0
- data/lib/pay/attributes.rb +16 -8
- data/lib/pay/braintree.rb +25 -6
- data/lib/pay/engine.rb +2 -0
- data/lib/pay/fake_processor.rb +2 -6
- data/lib/pay/lemon_squeezy/webhooks/order.rb +11 -0
- data/lib/pay/lemon_squeezy/webhooks/subscription.rb +3 -3
- data/lib/pay/lemon_squeezy/webhooks/subscription_payment.rb +11 -0
- data/lib/pay/lemon_squeezy.rb +58 -104
- data/lib/pay/nano_id.rb +1 -1
- data/lib/pay/paddle_billing.rb +15 -6
- data/lib/pay/paddle_classic/webhooks/signature_verifier.rb +1 -1
- data/lib/pay/paddle_classic.rb +11 -9
- data/lib/pay/receipts.rb +45 -44
- data/lib/pay/stripe/webhooks/charge_updated.rb +11 -0
- data/lib/pay/stripe/webhooks/customer_updated.rb +13 -9
- data/lib/pay/stripe/webhooks/payment_action_required.rb +10 -6
- data/lib/pay/stripe/webhooks/payment_failed.rb +6 -4
- data/lib/pay/stripe/webhooks/subscription_renewing.rb +9 -4
- data/lib/pay/stripe.rb +28 -9
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +19 -1
- data/lib/tasks/pay.rake +2 -2
- metadata +45 -43
- data/app/views/pay/stripe/_checkout_button.html.erb +0 -21
- data/lib/pay/braintree/authorization_error.rb +0 -9
- data/lib/pay/braintree/error.rb +0 -23
- data/lib/pay/fake_processor/charge.rb +0 -21
- data/lib/pay/fake_processor/error.rb +0 -6
- data/lib/pay/fake_processor/payment_method.rb +0 -21
- data/lib/pay/fake_processor/subscription.rb +0 -90
- data/lib/pay/lemon_squeezy/billable.rb +0 -90
- data/lib/pay/lemon_squeezy/charge.rb +0 -68
- data/lib/pay/lemon_squeezy/error.rb +0 -7
- data/lib/pay/lemon_squeezy/payment_method.rb +0 -40
- data/lib/pay/lemon_squeezy/subscription.rb +0 -185
- data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +0 -11
- data/lib/pay/paddle_billing/error.rb +0 -7
- data/lib/pay/paddle_classic/error.rb +0 -7
- data/lib/pay/stripe/charge.rb +0 -176
- data/lib/pay/stripe/error.rb +0 -7
@@ -0,0 +1,129 @@
|
|
1
|
+
module Pay
|
2
|
+
module LemonSqueezy
|
3
|
+
class Subscription < Pay::Subscription
|
4
|
+
def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
|
5
|
+
object ||= ::LemonSqueezy::Subscription.retrieve(id: subscription_id)
|
6
|
+
|
7
|
+
pay_customer = Pay::Customer.find_by(processor: :lemon_squeezy, processor_id: object.customer_id)
|
8
|
+
return unless pay_customer
|
9
|
+
|
10
|
+
attributes = {
|
11
|
+
current_period_end: object.renews_at,
|
12
|
+
ends_at: (object.ends_at ? Time.parse(object.ends_at) : nil),
|
13
|
+
pause_starts_at: (object.pause&.resumes_at ? Time.parse(object.pause.resumes_at) : nil),
|
14
|
+
status: object.status,
|
15
|
+
processor_plan: object.first_subscription_item.price_id,
|
16
|
+
quantity: object.first_subscription_item.quantity,
|
17
|
+
created_at: (object.created_at ? Time.parse(object.created_at) : nil),
|
18
|
+
updated_at: (object.updated_at ? Time.parse(object.updated_at) : nil)
|
19
|
+
}
|
20
|
+
|
21
|
+
case attributes[:status]
|
22
|
+
when "cancelled"
|
23
|
+
# Remove payment methods since customer cannot be reused after cancelling
|
24
|
+
Pay::PaymentMethod.where(customer_id: object.customer_id).destroy_all
|
25
|
+
when "on_trial"
|
26
|
+
attributes[:trial_ends_at] = Time.parse(object.trial_ends_at)
|
27
|
+
when "paused"
|
28
|
+
# attributes[:pause_starts_at] = Time.parse(object.paused_at)
|
29
|
+
when "active", "past_due"
|
30
|
+
attributes[:trial_ends_at] = nil
|
31
|
+
attributes[:pause_starts_at] = nil
|
32
|
+
attributes[:ends_at] = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# Update or create the subscription
|
36
|
+
if (pay_subscription = find_by(customer: pay_customer, processor_id: object.id))
|
37
|
+
pay_subscription.with_lock { pay_subscription.update!(attributes) }
|
38
|
+
pay_subscription
|
39
|
+
else
|
40
|
+
create!(attributes.merge(customer: pay_customer, name: name, processor_id: object.id))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def api_record(**options)
|
45
|
+
@api_record ||= ::LemonSqueezy::Subscription.retrieve(id: processor_id)
|
46
|
+
rescue ::LemonSqueezy::Error => e
|
47
|
+
raise Pay::LemonSqueezy::Error, e
|
48
|
+
end
|
49
|
+
|
50
|
+
def portal_url
|
51
|
+
api_record.urls.customer_portal
|
52
|
+
end
|
53
|
+
|
54
|
+
def update_url
|
55
|
+
api_record.urls.update_payment_method
|
56
|
+
end
|
57
|
+
|
58
|
+
def cancel(**options)
|
59
|
+
return if canceled?
|
60
|
+
response = ::LemonSqueezy::Subscription.cancel(id: processor_id)
|
61
|
+
update(status: response.status, ends_at: response.ends_at)
|
62
|
+
rescue ::LemonSqueezy::Error => e
|
63
|
+
raise Pay::LemonSqueezy::Error, e
|
64
|
+
end
|
65
|
+
|
66
|
+
def cancel_now!(**options)
|
67
|
+
raise Pay::Error, "Lemon Squeezy does not support cancelling immediately through the API."
|
68
|
+
end
|
69
|
+
|
70
|
+
def change_quantity(quantity, **options)
|
71
|
+
subscription_item = api_record.first_subscription_item
|
72
|
+
::LemonSqueezy::SubscriptionItem.update(id: subscription_item.id, quantity: quantity)
|
73
|
+
update(quantity: quantity)
|
74
|
+
rescue ::LemonSqueezy::Error => e
|
75
|
+
raise Pay::LemonSqueezy::Error, e
|
76
|
+
end
|
77
|
+
|
78
|
+
# A subscription could be set to cancel or pause in the future
|
79
|
+
# It is considered on grace period until the cancel or pause time begins
|
80
|
+
def on_grace_period?
|
81
|
+
(canceled? && Time.current < ends_at) || (paused? && pause_starts_at? && Time.current < pause_starts_at)
|
82
|
+
end
|
83
|
+
|
84
|
+
def paused?
|
85
|
+
status == "paused"
|
86
|
+
end
|
87
|
+
|
88
|
+
def pause(**options)
|
89
|
+
response = ::LemonSqueezy::Subscription.pause(id: processor_id, **options)
|
90
|
+
update!(status: :paused, pause_starts_at: response.pause&.resumes_at)
|
91
|
+
rescue ::LemonSqueezy::Error => e
|
92
|
+
raise Pay::LemonSqueezy::Error, e
|
93
|
+
end
|
94
|
+
|
95
|
+
def resumable?
|
96
|
+
paused? || canceled?
|
97
|
+
end
|
98
|
+
|
99
|
+
def resume
|
100
|
+
unless resumable?
|
101
|
+
raise Error, "You can only resume paused or cancelled subscriptions"
|
102
|
+
end
|
103
|
+
|
104
|
+
if paused? && pause_starts_at? && Time.current < pause_starts_at
|
105
|
+
::LemonSqueezy::Subscription.unpause(id: processor_id)
|
106
|
+
else
|
107
|
+
::LemonSqueezy::Subscription.uncancel(id: processor_id)
|
108
|
+
end
|
109
|
+
|
110
|
+
update(ends_at: nil, status: :active, pause_starts_at: nil)
|
111
|
+
rescue ::LemonSqueezy::Error => e
|
112
|
+
raise Pay::LemonSqueezy::Error, e
|
113
|
+
end
|
114
|
+
|
115
|
+
# Lemon Squeezy requires both the Product ID and Variant ID.
|
116
|
+
# The Variant ID will be saved as the processor_plan
|
117
|
+
def swap(plan, **options)
|
118
|
+
raise Error, "A plan_id is required to swap a subscription" unless plan
|
119
|
+
raise Error, "A variant_id is required to swap a subscription" unless options[:variant_id]
|
120
|
+
|
121
|
+
::LemonSqueezy::Subscription.change_plan id: processor_id, plan_id: plan, variant_id: options[:variant_id]
|
122
|
+
|
123
|
+
update(processor_plan: options[:variant_id], ends_at: nil, status: :active)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
ActiveSupport.run_load_hooks :pay_lemon_squeezy_subscription, Pay::LemonSqueezy::Subscription
|
data/app/models/pay/merchant.rb
CHANGED
@@ -6,19 +6,10 @@ module Pay
|
|
6
6
|
|
7
7
|
store_accessor :data, :onboarding_complete
|
8
8
|
|
9
|
-
delegate_missing_to :pay_processor
|
10
|
-
|
11
|
-
def self.pay_processor_for(name)
|
12
|
-
"Pay::#{name.to_s.classify}::Merchant".constantize
|
13
|
-
end
|
14
|
-
|
15
|
-
def pay_processor
|
16
|
-
return if processor.blank?
|
17
|
-
@pay_processor ||= self.class.pay_processor_for(processor).new(self)
|
18
|
-
end
|
19
|
-
|
20
9
|
def onboarding_complete?
|
21
10
|
ActiveModel::Type::Boolean.new.cast(data&.fetch("onboarding_complete")) || false
|
22
11
|
end
|
23
12
|
end
|
24
13
|
end
|
14
|
+
|
15
|
+
ActiveSupport.run_load_hooks :pay_merchant, Pay::Merchant
|
@@ -1,13 +1,7 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleBilling
|
3
|
-
class Charge
|
4
|
-
|
5
|
-
|
6
|
-
delegate :processor_id, :customer, to: :pay_charge
|
7
|
-
|
8
|
-
def initialize(pay_charge)
|
9
|
-
@pay_charge = pay_charge
|
10
|
-
end
|
3
|
+
class Charge < Pay::Charge
|
4
|
+
store_accessor :data, :paddle_receipt_url
|
11
5
|
|
12
6
|
def self.sync(charge_id, object: nil, try: 0, retries: 1)
|
13
7
|
# Skip loading the latest charge details from the API if we already have it
|
@@ -54,15 +48,23 @@ module Pay
|
|
54
48
|
end
|
55
49
|
|
56
50
|
# Update or create the charge
|
57
|
-
if (pay_charge =
|
58
|
-
pay_charge.with_lock
|
59
|
-
pay_charge.update!(attrs)
|
60
|
-
end
|
51
|
+
if (pay_charge = find_by(customer: pay_customer, processor_id: object.id))
|
52
|
+
pay_charge.with_lock { pay_charge.update!(attrs) }
|
61
53
|
pay_charge
|
62
54
|
else
|
63
|
-
|
55
|
+
create!(attrs.merge(customer: pay_customer, processor_id: object.id))
|
56
|
+
end
|
57
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
58
|
+
try += 1
|
59
|
+
if try <= retries
|
60
|
+
sleep 0.1
|
61
|
+
retry
|
62
|
+
else
|
63
|
+
raise
|
64
64
|
end
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|
68
68
|
end
|
69
|
+
|
70
|
+
ActiveSupport.run_load_hooks :pay_paddle_billing_charge, Pay::PaddleBilling::Charge
|
@@ -1,20 +1,12 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleBilling
|
3
|
-
class
|
4
|
-
|
3
|
+
class Customer < Pay::Customer
|
4
|
+
has_many :charges, dependent: :destroy, class_name: "Pay::PaddleBilling::Charge"
|
5
|
+
has_many :subscriptions, dependent: :destroy, class_name: "Pay::PaddleBilling::Subscription"
|
6
|
+
has_many :payment_methods, dependent: :destroy, class_name: "Pay::PaddleBilling::PaymentMethod"
|
7
|
+
has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::PaddleBilling::PaymentMethod"
|
5
8
|
|
6
|
-
|
7
|
-
:processor_id?,
|
8
|
-
:email,
|
9
|
-
:customer_name,
|
10
|
-
:card_token,
|
11
|
-
to: :pay_customer
|
12
|
-
|
13
|
-
def initialize(pay_customer)
|
14
|
-
@pay_customer = pay_customer
|
15
|
-
end
|
16
|
-
|
17
|
-
def customer_attributes
|
9
|
+
def api_record_attributes
|
18
10
|
{email: email, name: customer_name}
|
19
11
|
end
|
20
12
|
|
@@ -24,13 +16,16 @@ module Pay
|
|
24
16
|
# Creates a new Paddle::Customer using `email` and `customer_name` if empty processor_id
|
25
17
|
#
|
26
18
|
# Returns a Paddle::Customer object
|
27
|
-
def
|
19
|
+
def api_record
|
28
20
|
if processor_id?
|
29
21
|
::Paddle::Customer.retrieve(id: processor_id)
|
22
|
+
elsif (pc = ::Paddle::Customer.list(email: email).data&.first)
|
23
|
+
update!(processor_id: pc.id)
|
24
|
+
pc
|
30
25
|
else
|
31
|
-
|
32
|
-
|
33
|
-
|
26
|
+
pc = ::Paddle::Customer.create(email: email, name: customer_name)
|
27
|
+
update!(processor_id: pc.id)
|
28
|
+
pc
|
34
29
|
end
|
35
30
|
rescue ::Paddle::Error => e
|
36
31
|
raise Pay::PaddleBilling::Error, e
|
@@ -38,10 +33,9 @@ module Pay
|
|
38
33
|
|
39
34
|
# Syncs name and email to Paddle::Customer
|
40
35
|
# You can also pass in other attributes that will be merged into the default attributes
|
41
|
-
def
|
42
|
-
|
43
|
-
|
44
|
-
::Paddle::Customer.update(id: processor_id, **attrs)
|
36
|
+
def update_api_record(**attributes)
|
37
|
+
api_record unless processor_id?
|
38
|
+
::Paddle::Customer.update(id: processor_id, **api_record_attributes.merge(attributes))
|
45
39
|
end
|
46
40
|
|
47
41
|
def charge(amount, options = {})
|
@@ -58,7 +52,7 @@ module Pay
|
|
58
52
|
metadata: transaction.details.line_items&.first&.id
|
59
53
|
}
|
60
54
|
|
61
|
-
charge =
|
55
|
+
charge = charges.find_or_initialize_by(processor_id: transaction.id)
|
62
56
|
charge.update(attrs)
|
63
57
|
charge
|
64
58
|
rescue ::Paddle::Error => e
|
@@ -72,19 +66,10 @@ module Pay
|
|
72
66
|
# Paddle does not use payment method tokens. The method signature has it here
|
73
67
|
# to have a uniform API with the other payment processors.
|
74
68
|
def add_payment_method(token = nil, default: true)
|
75
|
-
Pay::PaddleBilling::PaymentMethod.sync(pay_customer:
|
76
|
-
end
|
77
|
-
|
78
|
-
def trial_end_date(subscription)
|
79
|
-
return unless subscription.state == "trialing"
|
80
|
-
Time.zone.parse(subscription.next_payment[:date]).end_of_day
|
81
|
-
end
|
82
|
-
|
83
|
-
def processor_subscription(subscription_id, options = {})
|
84
|
-
::Paddle::Subscription.retrieve(id: subscription_id, **options)
|
85
|
-
rescue ::Paddle::Error => e
|
86
|
-
raise Pay::PaddleBilling::Error, e
|
69
|
+
Pay::PaddleBilling::PaymentMethod.sync(pay_customer: self)
|
87
70
|
end
|
88
71
|
end
|
89
72
|
end
|
90
73
|
end
|
74
|
+
|
75
|
+
ActiveSupport.run_load_hooks :pay_paddle_billing_customer, Pay::PaddleBilling::Customer
|
@@ -1,10 +1,6 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleBilling
|
3
|
-
class PaymentMethod
|
4
|
-
attr_reader :pay_payment_method
|
5
|
-
|
6
|
-
delegate :customer, :processor_id, to: :pay_payment_method
|
7
|
-
|
3
|
+
class PaymentMethod < Pay::PaymentMethod
|
8
4
|
def self.sync_from_transaction(pay_customer:, transaction:)
|
9
5
|
transaction = ::Paddle::Transaction.retrieve(id: transaction)
|
10
6
|
return unless transaction.status == "completed"
|
@@ -12,10 +8,10 @@ module Pay
|
|
12
8
|
sync(pay_customer: pay_customer, attributes: transaction.payments.first)
|
13
9
|
end
|
14
10
|
|
15
|
-
def self.sync(pay_customer:, attributes:)
|
11
|
+
def self.sync(pay_customer:, attributes:, try: 0, retries: 1)
|
16
12
|
details = attributes.method_details
|
17
13
|
attrs = {
|
18
|
-
|
14
|
+
payment_method_type: details.type.downcase
|
19
15
|
}
|
20
16
|
|
21
17
|
case details.type.downcase
|
@@ -29,19 +25,23 @@ module Pay
|
|
29
25
|
payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.payment_method_id)
|
30
26
|
payment_method.update!(attrs)
|
31
27
|
payment_method
|
28
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
29
|
+
try += 1
|
30
|
+
if try <= retries
|
31
|
+
sleep 0.1
|
32
|
+
retry
|
33
|
+
else
|
34
|
+
raise
|
35
|
+
end
|
32
36
|
end
|
33
37
|
|
34
|
-
def initialize(pay_payment_method)
|
35
|
-
@pay_payment_method = pay_payment_method
|
36
|
-
end
|
37
|
-
|
38
|
-
# Sets payment method as default
|
39
38
|
def make_default!
|
40
39
|
end
|
41
40
|
|
42
|
-
# Remove payment method
|
43
41
|
def detach
|
44
42
|
end
|
45
43
|
end
|
46
44
|
end
|
47
45
|
end
|
46
|
+
|
47
|
+
ActiveSupport.run_load_hooks :pay_paddle_billing_payment_method, Pay::PaddleBilling::PaymentMethod
|
@@ -1,33 +1,15 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleBilling
|
3
|
-
class Subscription
|
4
|
-
|
5
|
-
|
6
|
-
delegate :active?,
|
7
|
-
:canceled?,
|
8
|
-
:on_grace_period?,
|
9
|
-
:on_trial?,
|
10
|
-
:ends_at,
|
11
|
-
:name,
|
12
|
-
:owner,
|
13
|
-
:pause_starts_at,
|
14
|
-
:pause_starts_at?,
|
15
|
-
:processor_id,
|
16
|
-
:processor_plan,
|
17
|
-
:processor_subscription,
|
18
|
-
:prorate,
|
19
|
-
:prorate?,
|
20
|
-
:quantity,
|
21
|
-
:quantity?,
|
22
|
-
:trial_ends_at,
|
23
|
-
to: :pay_subscription
|
3
|
+
class Subscription < Pay::Subscription
|
4
|
+
store_accessor :data, :paddle_update_url
|
5
|
+
store_accessor :data, :paddle_cancel_url
|
24
6
|
|
25
7
|
def self.sync_from_transaction(transaction_id)
|
26
8
|
transaction = ::Paddle::Transaction.retrieve(id: transaction_id)
|
27
9
|
sync(transaction.subscription_id) if transaction.subscription_id
|
28
10
|
end
|
29
11
|
|
30
|
-
def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
|
12
|
+
def self.sync(subscription_id, object: nil, name: Pay.default_product_name, try: 0, retries: 1)
|
31
13
|
# Passthrough is not return from this API, so we can't use that
|
32
14
|
object ||= ::Paddle::Subscription.retrieve(id: subscription_id)
|
33
15
|
|
@@ -75,22 +57,24 @@ module Pay
|
|
75
57
|
end
|
76
58
|
|
77
59
|
# Update or create the subscription
|
78
|
-
if (pay_subscription =
|
79
|
-
pay_subscription.with_lock
|
80
|
-
pay_subscription.update!(attributes)
|
81
|
-
end
|
60
|
+
if (pay_subscription = find_by(customer: pay_customer, processor_id: subscription_id))
|
61
|
+
pay_subscription.with_lock { pay_subscription.update!(attributes) }
|
82
62
|
pay_subscription
|
83
63
|
else
|
84
|
-
|
64
|
+
create!(attributes.merge(customer: pay_customer, name: name, processor_id: subscription_id))
|
65
|
+
end
|
66
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
67
|
+
try += 1
|
68
|
+
if try <= retries
|
69
|
+
sleep 0.1
|
70
|
+
retry
|
71
|
+
else
|
72
|
+
raise
|
85
73
|
end
|
86
74
|
end
|
87
75
|
|
88
|
-
def
|
89
|
-
@
|
90
|
-
end
|
91
|
-
|
92
|
-
def subscription(**options)
|
93
|
-
@paddle_billing_subscription ||= ::Paddle::Subscription.retrieve(id: processor_id, **options)
|
76
|
+
def api_record(**options)
|
77
|
+
@api_record ||= ::Paddle::Subscription.retrieve(id: processor_id, **options)
|
94
78
|
end
|
95
79
|
|
96
80
|
# Get a transaction to update payment method
|
@@ -105,9 +89,9 @@ module Pay
|
|
105
89
|
|
106
90
|
response = ::Paddle::Subscription.cancel(
|
107
91
|
id: processor_id,
|
108
|
-
effective_from: options.fetch(:effective_from,
|
92
|
+
effective_from: options.fetch(:effective_from, paused? ? "immediately" : "next_billing_period")
|
109
93
|
)
|
110
|
-
|
94
|
+
update(
|
111
95
|
status: response.status,
|
112
96
|
ends_at: response.scheduled_change&.effective_at || Time.current
|
113
97
|
)
|
@@ -116,7 +100,7 @@ module Pay
|
|
116
100
|
end
|
117
101
|
|
118
102
|
def cancel_now!(**options)
|
119
|
-
cancel(options.merge(effective_from: "immediately"))
|
103
|
+
cancel(**options.merge(effective_from: "immediately"))
|
120
104
|
rescue ::Paddle::Error => e
|
121
105
|
raise Pay::PaddleBilling::Error, e
|
122
106
|
end
|
@@ -127,7 +111,12 @@ module Pay
|
|
127
111
|
quantity: quantity
|
128
112
|
}]
|
129
113
|
|
130
|
-
::Paddle::Subscription.update(
|
114
|
+
::Paddle::Subscription.update(
|
115
|
+
id: processor_id,
|
116
|
+
items: items,
|
117
|
+
proration_billing_mode: options.delete(:proration_billing_mode) || "prorated_immediately"
|
118
|
+
)
|
119
|
+
update(quantity: quantity)
|
131
120
|
rescue ::Paddle::Error => e
|
132
121
|
raise Pay::PaddleBilling::Error, e
|
133
122
|
end
|
@@ -139,12 +128,12 @@ module Pay
|
|
139
128
|
end
|
140
129
|
|
141
130
|
def paused?
|
142
|
-
|
131
|
+
status == "paused"
|
143
132
|
end
|
144
133
|
|
145
134
|
def pause
|
146
135
|
response = ::Paddle::Subscription.pause(id: processor_id)
|
147
|
-
|
136
|
+
update!(status: :paused, pause_starts_at: response.scheduled_change.effective_at)
|
148
137
|
rescue ::Paddle::Error => e
|
149
138
|
raise Pay::PaddleBilling::Error, e
|
150
139
|
end
|
@@ -155,7 +144,7 @@ module Pay
|
|
155
144
|
|
156
145
|
def resume
|
157
146
|
unless resumable?
|
158
|
-
raise
|
147
|
+
raise Error, "You can only resume paused subscriptions."
|
159
148
|
end
|
160
149
|
|
161
150
|
# Paddle Billing API only allows "resuming" subscriptions when they are paused
|
@@ -166,19 +155,25 @@ module Pay
|
|
166
155
|
::Paddle::Subscription.resume(id: processor_id, effective_from: "immediately")
|
167
156
|
end
|
168
157
|
|
169
|
-
|
158
|
+
update(ends_at: nil, status: :active, pause_starts_at: nil)
|
170
159
|
rescue ::Paddle::Error => e
|
171
160
|
raise Pay::PaddleBilling::Error, e
|
172
161
|
end
|
173
162
|
|
174
163
|
def swap(plan, **options)
|
164
|
+
raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
|
165
|
+
|
175
166
|
items = [{
|
176
167
|
price_id: plan,
|
177
168
|
quantity: quantity || 1
|
178
169
|
}]
|
179
170
|
|
180
|
-
::Paddle::Subscription.update(
|
181
|
-
|
171
|
+
::Paddle::Subscription.update(
|
172
|
+
id: processor_id,
|
173
|
+
items: items,
|
174
|
+
proration_billing_mode: options.delete(:proration_billing_mode) || "prorated_immediately"
|
175
|
+
)
|
176
|
+
update(processor_plan: plan, ends_at: nil, status: :active)
|
182
177
|
end
|
183
178
|
|
184
179
|
# Retries the latest invoice for a Past Due subscription
|
@@ -187,3 +182,5 @@ module Pay
|
|
187
182
|
end
|
188
183
|
end
|
189
184
|
end
|
185
|
+
|
186
|
+
ActiveSupport.run_load_hooks :pay_paddle_billing_subscription, Pay::PaddleBilling::Subscription
|
@@ -1,35 +1,32 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleClassic
|
3
|
-
class Charge
|
4
|
-
|
3
|
+
class Charge < Pay::Charge
|
4
|
+
store_accessor :data, :paddle_receipt_url
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(pay_charge)
|
9
|
-
@pay_charge = pay_charge
|
10
|
-
end
|
11
|
-
|
12
|
-
def charge
|
6
|
+
def api_record
|
13
7
|
return unless customer.subscription
|
8
|
+
|
14
9
|
payments = PaddleClassic.client.payments.list(subscription_id: customer.subscription.processor_id)
|
15
10
|
charges = payments.data.select { |p| p[:id].to_s == processor_id }
|
16
11
|
charges.try(:first)
|
17
|
-
rescue ::Paddle::Error => e
|
12
|
+
rescue ::Paddle::Classic::Error => e
|
18
13
|
raise Pay::PaddleClassic::Error, e
|
19
14
|
end
|
20
15
|
|
21
|
-
def refund!(amount_to_refund)
|
16
|
+
def refund!(amount_to_refund = nil)
|
22
17
|
return unless customer.subscription
|
18
|
+
amount_to_refund ||= amount
|
19
|
+
|
23
20
|
payments = PaddleClassic.client.payments.list(subscription_id: customer.subscription.processor_id, is_paid: 1)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
end
|
30
|
-
rescue ::Paddle::Error => e
|
21
|
+
raise Error, "Payment not found" unless payments.total > 0
|
22
|
+
|
23
|
+
PaddleClassic.client.payments.refund(order_id: payments.data.last[:id], amount: amount_to_refund)
|
24
|
+
update(amount_refunded: amount_to_refund)
|
25
|
+
rescue ::Paddle::Classic::Error => e
|
31
26
|
raise Pay::PaddleClassic::Error, e
|
32
27
|
end
|
33
28
|
end
|
34
29
|
end
|
35
30
|
end
|
31
|
+
|
32
|
+
ActiveSupport.run_load_hooks :pay_paddle_classic_charge, Pay::PaddleClassic::Charge
|
@@ -1,29 +1,18 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleClassic
|
3
|
-
class
|
4
|
-
|
3
|
+
class Customer < Pay::Customer
|
4
|
+
has_many :charges, dependent: :destroy, class_name: "Pay::PaddleClassic::Charge"
|
5
|
+
has_many :subscriptions, dependent: :destroy, class_name: "Pay::PaddleClassic::Subscription"
|
6
|
+
has_many :payment_methods, dependent: :destroy, class_name: "Pay::PaddleClassic::PaymentMethod"
|
7
|
+
has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::PaddleClassic::PaymentMethod"
|
5
8
|
|
6
|
-
|
7
|
-
:processor_id?,
|
8
|
-
:email,
|
9
|
-
:customer_name,
|
10
|
-
:card_token,
|
11
|
-
to: :pay_customer
|
12
|
-
|
13
|
-
def initialize(pay_customer)
|
14
|
-
@pay_customer = pay_customer
|
15
|
-
end
|
16
|
-
|
17
|
-
def customer
|
18
|
-
# pass
|
9
|
+
def api_record
|
19
10
|
end
|
20
11
|
|
21
|
-
def
|
22
|
-
# pass
|
12
|
+
def update_api_record
|
23
13
|
end
|
24
14
|
|
25
15
|
def charge(amount, options = {})
|
26
|
-
subscription = pay_customer.subscription
|
27
16
|
return unless subscription.processor_id
|
28
17
|
raise Pay::Error, "A charge_name is required to create a one-time charge" if options[:charge_name].nil?
|
29
18
|
|
@@ -38,7 +27,7 @@ module Pay
|
|
38
27
|
# Lookup subscription payment method details
|
39
28
|
attributes.merge! Pay::PaddleClassic::PaymentMethod.payment_method_details_for(subscription_id: subscription.processor_id)
|
40
29
|
|
41
|
-
charge =
|
30
|
+
charge = charges.find_or_initialize_by(processor_id: response[:invoice_id])
|
42
31
|
charge.update(attributes)
|
43
32
|
charge
|
44
33
|
rescue ::Paddle::Error => e
|
@@ -52,19 +41,10 @@ module Pay
|
|
52
41
|
# Paddle does not use payment method tokens. The method signature has it here
|
53
42
|
# to have a uniform API with the other payment processors.
|
54
43
|
def add_payment_method(token = nil, default: true)
|
55
|
-
Pay::PaddleClassic::PaymentMethod.sync(pay_customer:
|
56
|
-
end
|
57
|
-
|
58
|
-
def trial_end_date(subscription)
|
59
|
-
return unless subscription.state == "trialing"
|
60
|
-
Time.zone.parse(subscription.next_payment[:date]).end_of_day
|
61
|
-
end
|
62
|
-
|
63
|
-
def processor_subscription(subscription_id, options = {})
|
64
|
-
PaddleClassic.client.users.list(subscription_id: subscription_id).data.try(:first)
|
65
|
-
rescue ::Paddle::Error => e
|
66
|
-
raise Pay::PaddleClassic::Error, e
|
44
|
+
Pay::PaddleClassic::PaymentMethod.sync(pay_customer: self)
|
67
45
|
end
|
68
46
|
end
|
69
47
|
end
|
70
48
|
end
|
49
|
+
|
50
|
+
ActiveSupport.run_load_hooks :pay_paddle_classic_customer, Pay::PaddleClassic::Customer
|
@@ -1,10 +1,6 @@
|
|
1
1
|
module Pay
|
2
2
|
module PaddleClassic
|
3
|
-
class PaymentMethod
|
4
|
-
attr_reader :pay_payment_method
|
5
|
-
|
6
|
-
delegate :customer, :processor_id, to: :pay_payment_method
|
7
|
-
|
3
|
+
class PaymentMethod < Pay::PaymentMethod
|
8
4
|
# Paddle doesn't provide PaymentMethod IDs, so we have to lookup via the Customer
|
9
5
|
def self.sync(pay_customer:, attributes: nil)
|
10
6
|
return unless pay_customer.subscription
|
@@ -44,17 +40,13 @@ module Pay
|
|
44
40
|
end
|
45
41
|
end
|
46
42
|
|
47
|
-
def initialize(pay_payment_method)
|
48
|
-
@pay_payment_method = pay_payment_method
|
49
|
-
end
|
50
|
-
|
51
|
-
# Sets payment method as default
|
52
43
|
def make_default!
|
53
44
|
end
|
54
45
|
|
55
|
-
# Remove payment method
|
56
46
|
def detach
|
57
47
|
end
|
58
48
|
end
|
59
49
|
end
|
60
50
|
end
|
51
|
+
|
52
|
+
ActiveSupport.run_load_hooks :pay_paddle_classic_payment_method, Pay::PaddleClassic::PaymentMethod
|