pay 11.8.0 → 12.1.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.
- checksums.yaml +4 -4
- data/app/controllers/pay/payments_controller.rb +3 -11
- data/app/controllers/pay/webhooks/base_controller.rb +50 -0
- data/app/controllers/pay/webhooks/braintree_controller.rb +11 -18
- data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +8 -26
- data/app/controllers/pay/webhooks/paddle_billing_controller.rb +13 -35
- data/app/controllers/pay/webhooks/paddle_classic_controller.rb +5 -20
- data/app/controllers/pay/webhooks/stripe_controller.rb +13 -29
- data/app/models/pay/braintree/charge.rb +6 -12
- data/app/models/pay/braintree/payment_method.rb +7 -5
- data/app/models/pay/braintree/subscription.rb +39 -44
- data/app/models/pay/charge.rb +1 -1
- data/app/models/pay/customer.rb +1 -9
- data/app/models/pay/lemon_squeezy/charge.rb +63 -61
- data/app/models/pay/lemon_squeezy/customer.rb +1 -1
- data/app/models/pay/lemon_squeezy/subscription.rb +43 -46
- data/app/models/pay/merchant.rb +1 -1
- data/app/models/pay/paddle_billing/charge.rb +43 -53
- data/app/models/pay/paddle_billing/customer.rb +1 -1
- data/app/models/pay/paddle_billing/payment_method.rb +19 -23
- data/app/models/pay/paddle_billing/subscription.rb +56 -67
- data/app/models/pay/paddle_classic/customer.rb +1 -1
- data/app/models/pay/paddle_classic/subscription.rb +42 -44
- data/app/models/pay/payment_method.rb +1 -5
- data/app/models/pay/stripe/charge.rb +49 -61
- data/app/models/pay/stripe/customer.rb +41 -19
- data/app/models/pay/stripe/payment_method.rb +25 -30
- data/app/models/pay/stripe/subscription.rb +103 -107
- data/app/models/pay/subscription.rb +2 -12
- data/app/views/layouts/pay/application.html.erb +0 -2
- data/app/views/pay/payments/show.html.erb +19 -8
- data/lib/pay/braintree/webhooks/subscription.rb +15 -0
- data/lib/pay/braintree/webhooks/subscription_canceled.rb +1 -7
- data/lib/pay/braintree/webhooks/subscription_expired.rb +1 -7
- data/lib/pay/braintree/webhooks/subscription_trial_ended.rb +1 -7
- data/lib/pay/braintree/webhooks/subscription_went_active.rb +2 -8
- data/lib/pay/braintree/webhooks/subscription_went_past_due.rb +2 -8
- data/lib/pay/braintree.rb +1 -2
- data/lib/pay/currency.rb +0 -4
- data/lib/pay/engine.rb +7 -1
- data/lib/pay/errors.rb +4 -0
- data/lib/pay/lemon_squeezy.rb +4 -7
- data/lib/pay/paddle_billing.rb +4 -1
- data/lib/pay/paddle_classic.rb +4 -1
- data/lib/pay/payment.rb +2 -12
- data/lib/pay/receipts.rb +0 -1
- data/lib/pay/stripe/webhooks/subscription_created.rb +2 -4
- data/lib/pay/stripe/webhooks/subscription_deleted.rb +2 -5
- data/lib/pay/stripe.rb +2 -2
- data/lib/pay/sync.rb +29 -0
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +7 -5
- metadata +5 -3
- data/lib/pay/braintree/webhooks/subscription_charged_unsuccessfully.rb +0 -24
|
@@ -1,80 +1,82 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module LemonSqueezy
|
|
3
3
|
class Charge < Pay::Charge
|
|
4
|
+
extend Pay::Sync
|
|
5
|
+
|
|
4
6
|
# LemonSqueezy uses Order for one-time payments and Order + Subscription + SubscriptionInvoice for subscriptions
|
|
7
|
+
# Charges are stored with a "order:123" or "subscription_invoice:123" processor_id so both can be synced by ID
|
|
5
8
|
|
|
6
|
-
def self.
|
|
7
|
-
|
|
9
|
+
def self.sync(processor_id, object: nil)
|
|
10
|
+
type, id = processor_id.split(":", 2)
|
|
11
|
+
case type
|
|
12
|
+
when "order"
|
|
13
|
+
sync_order(id, object: object)
|
|
14
|
+
when "subscription_invoice"
|
|
15
|
+
sync_subscription_invoice(id, object: object)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
8
18
|
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
def self.sync_order(order_id, object: nil)
|
|
20
|
+
sync_with_retries do
|
|
21
|
+
order = object || ::LemonSqueezy::Order.retrieve(id: order_id)
|
|
22
|
+
return unless (pay_customer = find_pay_customer(order.customer_id))
|
|
11
23
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
processor_id = "order:#{order.id}"
|
|
25
|
+
attributes = {
|
|
26
|
+
processor_id: processor_id,
|
|
27
|
+
currency: order.currency,
|
|
28
|
+
subtotal: order.subtotal,
|
|
29
|
+
tax: order.tax,
|
|
30
|
+
amount: order.total,
|
|
31
|
+
amount_refunded: order.refunded_amount,
|
|
32
|
+
created_at: (order.created_at ? Time.parse(order.created_at) : nil),
|
|
33
|
+
updated_at: (order.updated_at ? Time.parse(order.updated_at) : nil)
|
|
34
|
+
}
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
# Update or create the charge
|
|
37
|
+
if (pay_charge = find_by(customer: pay_customer, processor_id: processor_id))
|
|
38
|
+
pay_charge.with_lock { pay_charge.update!(attributes) }
|
|
39
|
+
pay_charge
|
|
40
|
+
else
|
|
41
|
+
create!(attributes.merge(customer: pay_customer, processor_id: processor_id))
|
|
42
|
+
end
|
|
30
43
|
end
|
|
31
44
|
end
|
|
32
45
|
|
|
33
46
|
def self.sync_subscription_invoice(subscription_invoice_id, object: nil)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
pay_customer = Pay::LemonSqueezy::Customer.find_by(processor_id: object.customer_id)
|
|
38
|
-
return unless pay_customer
|
|
47
|
+
sync_with_retries do
|
|
48
|
+
invoice = object || ::LemonSqueezy::SubscriptionInvoice.retrieve(id: subscription_invoice_id)
|
|
49
|
+
return unless (pay_customer = find_pay_customer(invoice.customer_id))
|
|
39
50
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
processor_id = "subscription_invoice:#{invoice.id}"
|
|
52
|
+
subscription = Pay::LemonSqueezy::Subscription.find_by(processor_id: invoice.subscription_id)
|
|
53
|
+
attributes = {
|
|
54
|
+
processor_id: processor_id,
|
|
55
|
+
currency: invoice.currency,
|
|
56
|
+
amount: invoice.total,
|
|
57
|
+
amount_refunded: invoice.refunded_amount,
|
|
58
|
+
subtotal: invoice.subtotal,
|
|
59
|
+
tax: invoice.tax,
|
|
60
|
+
subscription: subscription,
|
|
61
|
+
payment_method_type: ("card" if invoice.card_brand.present?),
|
|
62
|
+
brand: invoice.card_brand,
|
|
63
|
+
last4: invoice.card_last_four,
|
|
64
|
+
created_at: (invoice.created_at ? Time.parse(invoice.created_at) : nil),
|
|
65
|
+
updated_at: (invoice.updated_at ? Time.parse(invoice.updated_at) : nil)
|
|
66
|
+
}
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
# Update customer's payment method
|
|
69
|
+
Pay::LemonSqueezy::PaymentMethod.sync(pay_customer: pay_customer, attributes: invoice)
|
|
59
70
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
# Update or create the charge
|
|
72
|
+
if (pay_charge = pay_customer.charges.find_by(processor_id: processor_id))
|
|
73
|
+
pay_charge.with_lock do
|
|
74
|
+
pay_charge.update!(attributes)
|
|
75
|
+
end
|
|
76
|
+
pay_charge
|
|
77
|
+
else
|
|
78
|
+
pay_customer.charges.create!(attributes.merge(processor_id: processor_id))
|
|
64
79
|
end
|
|
65
|
-
pay_charge
|
|
66
|
-
else
|
|
67
|
-
pay_customer.charges.create!(attributes.merge(processor_id: processor_id))
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def save
|
|
72
|
-
ls_type, ls_id = processor_id.split(":", 2)
|
|
73
|
-
case ls_type
|
|
74
|
-
when "order"
|
|
75
|
-
self.class.sync_order(ls_id)
|
|
76
|
-
when "subscription_invoice"
|
|
77
|
-
self.class.sync_subscription_invoice(ls_id)
|
|
78
80
|
end
|
|
79
81
|
end
|
|
80
82
|
|
|
@@ -41,7 +41,7 @@ module Pay
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def charge(amount, options = {})
|
|
44
|
-
raise Pay::
|
|
44
|
+
raise Pay::NotSupportedError, "Lemon Squeezy does not support one-off charges"
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
|
@@ -1,43 +1,46 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module LemonSqueezy
|
|
3
3
|
class Subscription < Pay::Subscription
|
|
4
|
-
|
|
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
|
|
4
|
+
extend Pay::Sync
|
|
34
5
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
6
|
+
# Lemon Squeezy statuses that Pay spells differently
|
|
7
|
+
STATUSES = {"on_trial" => "trialing", "cancelled" => "canceled"}.freeze
|
|
8
|
+
|
|
9
|
+
def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
|
|
10
|
+
sync_with_retries do
|
|
11
|
+
subscription = object || ::LemonSqueezy::Subscription.retrieve(id: subscription_id)
|
|
12
|
+
return unless (pay_customer = find_pay_customer(subscription.customer_id))
|
|
13
|
+
|
|
14
|
+
attributes = {
|
|
15
|
+
current_period_end: subscription.renews_at,
|
|
16
|
+
ends_at: (subscription.ends_at ? Time.parse(subscription.ends_at) : nil),
|
|
17
|
+
pause_resumes_at: (subscription.pause&.resumes_at ? Time.parse(subscription.pause.resumes_at) : nil),
|
|
18
|
+
status: STATUSES.fetch(subscription.status, subscription.status),
|
|
19
|
+
processor_plan: subscription.first_subscription_item.price_id,
|
|
20
|
+
quantity: subscription.first_subscription_item.quantity,
|
|
21
|
+
created_at: (subscription.created_at ? Time.parse(subscription.created_at) : nil),
|
|
22
|
+
updated_at: (subscription.updated_at ? Time.parse(subscription.updated_at) : nil)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
case attributes[:status]
|
|
26
|
+
when "canceled"
|
|
27
|
+
# Remove payment methods since customer cannot be reused after cancelling
|
|
28
|
+
pay_customer.payment_methods.destroy_all
|
|
29
|
+
when "trialing"
|
|
30
|
+
attributes[:trial_ends_at] = Time.parse(subscription.trial_ends_at)
|
|
31
|
+
when "active", "past_due"
|
|
32
|
+
attributes[:trial_ends_at] = nil
|
|
33
|
+
attributes[:pause_resumes_at] = nil
|
|
34
|
+
attributes[:ends_at] = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Update or create the subscription
|
|
38
|
+
if (pay_subscription = find_by(customer: pay_customer, processor_id: subscription.id))
|
|
39
|
+
pay_subscription.with_lock { pay_subscription.update!(attributes) }
|
|
40
|
+
pay_subscription
|
|
41
|
+
else
|
|
42
|
+
create!(attributes.merge(customer: pay_customer, name: name, processor_id: subscription.id))
|
|
43
|
+
end
|
|
41
44
|
end
|
|
42
45
|
end
|
|
43
46
|
|
|
@@ -64,7 +67,7 @@ module Pay
|
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def cancel_now!(**options)
|
|
67
|
-
raise Pay::
|
|
70
|
+
raise Pay::NotSupportedError, "Lemon Squeezy does not support cancelling immediately through the API"
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
def change_quantity(quantity, **options)
|
|
@@ -75,19 +78,13 @@ module Pay
|
|
|
75
78
|
raise Pay::LemonSqueezy::Error, e
|
|
76
79
|
end
|
|
77
80
|
|
|
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
81
|
def paused?
|
|
85
82
|
status == "paused"
|
|
86
83
|
end
|
|
87
84
|
|
|
88
85
|
def pause(**options)
|
|
89
86
|
response = ::LemonSqueezy::Subscription.pause(id: processor_id, **options)
|
|
90
|
-
update!(status: :paused,
|
|
87
|
+
update!(status: :paused, pause_resumes_at: response.pause&.resumes_at)
|
|
91
88
|
rescue ::LemonSqueezy::Error => e
|
|
92
89
|
raise Pay::LemonSqueezy::Error, e
|
|
93
90
|
end
|
|
@@ -101,13 +98,13 @@ module Pay
|
|
|
101
98
|
raise Error, "You can only resume paused or cancelled subscriptions"
|
|
102
99
|
end
|
|
103
100
|
|
|
104
|
-
if paused?
|
|
101
|
+
if paused?
|
|
105
102
|
::LemonSqueezy::Subscription.unpause(id: processor_id)
|
|
106
103
|
else
|
|
107
104
|
::LemonSqueezy::Subscription.uncancel(id: processor_id)
|
|
108
105
|
end
|
|
109
106
|
|
|
110
|
-
update(ends_at: nil, status: :active,
|
|
107
|
+
update(ends_at: nil, status: :active, pause_resumes_at: nil)
|
|
111
108
|
rescue ::LemonSqueezy::Error => e
|
|
112
109
|
raise Pay::LemonSqueezy::Error, e
|
|
113
110
|
end
|
data/app/models/pay/merchant.rb
CHANGED
|
@@ -1,66 +1,56 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module PaddleBilling
|
|
3
3
|
class Charge < Pay::Charge
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def self.sync(charge_id, object: nil, try: 0, retries: 1)
|
|
7
|
-
# Skip loading the latest charge details from the API if we already have it
|
|
8
|
-
object ||= ::Paddle::Transaction.retrieve(id: charge_id)
|
|
9
|
-
|
|
10
|
-
# Ignore transactions that aren't completed
|
|
11
|
-
return unless object.status == "completed"
|
|
4
|
+
extend Pay::Sync
|
|
12
5
|
|
|
13
|
-
|
|
14
|
-
return if object.customer_id.blank?
|
|
15
|
-
|
|
16
|
-
pay_customer = Pay::Customer.find_by(processor: :paddle_billing, processor_id: object.customer_id)
|
|
17
|
-
return unless pay_customer
|
|
6
|
+
store_accessor :data, :paddle_receipt_url
|
|
18
7
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
|
|
23
|
-
return
|
|
24
|
-
end
|
|
8
|
+
def self.sync(charge_id, object: nil)
|
|
9
|
+
sync_with_retries do
|
|
10
|
+
transaction = object || ::Paddle::Transaction.retrieve(id: charge_id)
|
|
25
11
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
currency: object.currency_code,
|
|
30
|
-
metadata: object.details.line_items&.first&.id,
|
|
31
|
-
subscription: pay_customer.subscriptions.find_by(processor_id: object.subscription_id)
|
|
32
|
-
}
|
|
12
|
+
# Ignore transactions that aren't completed
|
|
13
|
+
return unless transaction.status == "completed"
|
|
14
|
+
return unless (pay_customer = find_pay_customer(transaction.customer_id))
|
|
33
15
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
attrs[:exp_month] = details.card.expiry_month
|
|
40
|
-
attrs[:exp_year] = details.card.expiry_year
|
|
41
|
-
attrs[:last4] = details.card.last4
|
|
42
|
-
when "paypal"
|
|
43
|
-
attrs[:payment_method_type] = "paypal"
|
|
16
|
+
# Ignore transactions that are payment method changes
|
|
17
|
+
# But update the customer's payment method
|
|
18
|
+
if transaction.origin == "subscription_payment_method_change"
|
|
19
|
+
Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: transaction.payments.first)
|
|
20
|
+
return
|
|
44
21
|
end
|
|
45
22
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
23
|
+
attrs = {
|
|
24
|
+
amount: transaction.details.totals.grand_total,
|
|
25
|
+
created_at: transaction.created_at,
|
|
26
|
+
currency: transaction.currency_code,
|
|
27
|
+
metadata: transaction.details.line_items&.first&.id,
|
|
28
|
+
subscription: pay_customer.subscriptions.find_by(processor_id: transaction.subscription_id)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (details = Array.wrap(transaction.payments).first&.method_details)
|
|
32
|
+
case details.type.downcase
|
|
33
|
+
when "card"
|
|
34
|
+
attrs[:payment_method_type] = "card"
|
|
35
|
+
attrs[:brand] = details.card.type
|
|
36
|
+
attrs[:exp_month] = details.card.expiry_month
|
|
37
|
+
attrs[:exp_year] = details.card.expiry_year
|
|
38
|
+
attrs[:last4] = details.card.last4
|
|
39
|
+
when "paypal"
|
|
40
|
+
attrs[:payment_method_type] = "paypal"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Update customer's payment method
|
|
44
|
+
Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: transaction.payments.first)
|
|
45
|
+
end
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
|
58
|
-
try += 1
|
|
59
|
-
if try <= retries
|
|
60
|
-
sleep 0.1
|
|
61
|
-
retry
|
|
62
|
-
else
|
|
63
|
-
raise
|
|
47
|
+
# Update or create the charge
|
|
48
|
+
if (pay_charge = find_by(customer: pay_customer, processor_id: transaction.id))
|
|
49
|
+
pay_charge.with_lock { pay_charge.update!(attrs) }
|
|
50
|
+
pay_charge
|
|
51
|
+
else
|
|
52
|
+
create!(attrs.merge(customer: pay_customer, processor_id: transaction.id))
|
|
53
|
+
end
|
|
64
54
|
end
|
|
65
55
|
end
|
|
66
56
|
end
|
|
@@ -60,7 +60,7 @@ module Pay
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
|
63
|
-
|
|
63
|
+
raise Pay::NotSupportedError, "Paddle Billing subscriptions are created with Paddle Checkout and synced from webhooks"
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# Paddle does not use payment method tokens. The method signature has it here
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module PaddleBilling
|
|
3
3
|
class PaymentMethod < Pay::PaymentMethod
|
|
4
|
+
extend Pay::Sync
|
|
5
|
+
|
|
4
6
|
def self.sync_from_transaction(pay_customer:, transaction:)
|
|
5
7
|
transaction = ::Paddle::Transaction.retrieve(id: transaction)
|
|
6
8
|
return unless transaction.status == "completed"
|
|
@@ -8,30 +10,24 @@ module Pay
|
|
|
8
10
|
sync(pay_customer: pay_customer, attributes: transaction.payments.first)
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
def self.sync(pay_customer:, attributes
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
def self.sync(pay_customer:, attributes:)
|
|
14
|
+
sync_with_retries do
|
|
15
|
+
details = attributes.method_details
|
|
16
|
+
attrs = {
|
|
17
|
+
payment_method_type: details.type.downcase
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
case details.type.downcase
|
|
21
|
+
when "card"
|
|
22
|
+
attrs[:brand] = details.card.type
|
|
23
|
+
attrs[:last4] = details.card.last4
|
|
24
|
+
attrs[:exp_month] = details.card.expiry_month
|
|
25
|
+
attrs[:exp_year] = details.card.expiry_year
|
|
26
|
+
end
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
|
29
|
-
try += 1
|
|
30
|
-
if try <= retries
|
|
31
|
-
sleep 0.1
|
|
32
|
-
retry
|
|
33
|
-
else
|
|
34
|
-
raise
|
|
28
|
+
payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.payment_method_id)
|
|
29
|
+
payment_method.update!(attrs)
|
|
30
|
+
payment_method
|
|
35
31
|
end
|
|
36
32
|
end
|
|
37
33
|
|
|
@@ -1,75 +1,68 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module PaddleBilling
|
|
3
3
|
class Subscription < Pay::Subscription
|
|
4
|
+
extend Pay::Sync
|
|
5
|
+
|
|
4
6
|
store_accessor :data, :paddle_update_url
|
|
5
7
|
store_accessor :data, :paddle_cancel_url
|
|
6
8
|
|
|
7
9
|
def self.sync_from_transaction(transaction_id)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
Pay.deprecator.warn "Pay::PaddleBilling::Subscription.sync_from_transaction is deprecated, use Pay::PaddleBilling.sync_transaction instead"
|
|
11
|
+
Pay::PaddleBilling.sync_transaction(transaction_id)
|
|
10
12
|
end
|
|
11
13
|
|
|
12
|
-
def self.sync(subscription_id, object: nil, name: Pay.default_product_name
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
14
|
+
def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
|
|
15
|
+
sync_with_retries do
|
|
16
|
+
subscription = object || ::Paddle::Subscription.retrieve(id: subscription_id)
|
|
17
|
+
return unless (pay_customer = find_pay_customer(subscription.customer_id))
|
|
18
|
+
|
|
19
|
+
attributes = {
|
|
20
|
+
current_period_end: subscription.current_billing_period&.ends_at,
|
|
21
|
+
current_period_start: subscription.current_billing_period&.starts_at,
|
|
22
|
+
ends_at: (subscription.canceled_at ? Time.parse(subscription.canceled_at) : nil),
|
|
23
|
+
metadata: subscription.custom_data,
|
|
24
|
+
paddle_cancel_url: subscription.management_urls&.cancel,
|
|
25
|
+
paddle_update_url: subscription.management_urls&.update_payment_method,
|
|
26
|
+
pause_starts_at: (subscription.paused_at ? Time.parse(subscription.paused_at) : nil),
|
|
27
|
+
status: subscription.status
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if subscription.items&.first
|
|
31
|
+
item = subscription.items.first
|
|
32
|
+
attributes[:processor_plan] = item.price.id
|
|
33
|
+
attributes[:quantity] = item.quantity
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
case attributes[:status]
|
|
37
|
+
when "canceled"
|
|
38
|
+
# Remove payment methods since customer cannot be reused after cancelling
|
|
39
|
+
pay_customer.payment_methods.destroy_all
|
|
40
|
+
when "trialing"
|
|
41
|
+
attributes[:trial_ends_at] = Time.parse(subscription.next_billed_at) if subscription.next_billed_at
|
|
42
|
+
when "paused"
|
|
43
|
+
attributes[:pause_starts_at] = Time.parse(subscription.paused_at) if subscription.paused_at
|
|
44
|
+
when "active", "past_due"
|
|
45
|
+
attributes[:trial_ends_at] = nil
|
|
46
|
+
attributes[:pause_starts_at] = nil
|
|
47
|
+
attributes[:ends_at] = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
case subscription.scheduled_change&.action
|
|
51
|
+
when "cancel"
|
|
52
|
+
attributes[:ends_at] = Time.parse(subscription.scheduled_change.effective_at)
|
|
53
|
+
when "pause"
|
|
54
|
+
attributes[:pause_starts_at] = Time.parse(subscription.scheduled_change.effective_at)
|
|
55
|
+
when "resume"
|
|
56
|
+
attributes[:pause_resumes_at] = Time.parse(subscription.scheduled_change.effective_at)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Update or create the subscription
|
|
60
|
+
if (pay_subscription = find_by(customer: pay_customer, processor_id: subscription_id))
|
|
61
|
+
pay_subscription.with_lock { pay_subscription.update!(attributes) }
|
|
62
|
+
pay_subscription
|
|
63
|
+
else
|
|
64
|
+
create!(attributes.merge(customer: pay_customer, name: name, processor_id: subscription_id))
|
|
65
|
+
end
|
|
73
66
|
end
|
|
74
67
|
end
|
|
75
68
|
|
|
@@ -175,10 +168,6 @@ module Pay
|
|
|
175
168
|
)
|
|
176
169
|
update(processor_plan: plan, ends_at: nil, status: :active)
|
|
177
170
|
end
|
|
178
|
-
|
|
179
|
-
# Retries the latest invoice for a Past Due subscription
|
|
180
|
-
def retry_failed_payment
|
|
181
|
-
end
|
|
182
171
|
end
|
|
183
172
|
end
|
|
184
173
|
end
|
|
@@ -35,7 +35,7 @@ module Pay
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
|
38
|
-
|
|
38
|
+
raise Pay::NotSupportedError, "Paddle Classic subscriptions are created with Paddle Checkout and synced from webhooks"
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# Paddle does not use payment method tokens. The method signature has it here
|