pay 7.2.1 → 8.0.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/README.md +2 -0
- data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +45 -0
- data/app/controllers/pay/webhooks/stripe_controller.rb +2 -1
- 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 +5 -12
- data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +31 -71
- data/{lib → app/models}/pay/braintree/payment_method.rb +1 -9
- data/{lib → app/models}/pay/braintree/subscription.rb +14 -52
- data/app/models/pay/charge.rb +8 -27
- data/app/models/pay/customer.rb +2 -15
- data/app/models/pay/fake_processor/charge.rb +13 -0
- data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +22 -35
- data/{lib → app/models}/pay/fake_processor/merchant.rb +2 -9
- data/app/models/pay/fake_processor/payment_method.rb +11 -0
- data/app/models/pay/fake_processor/subscription.rb +60 -0
- data/app/models/pay/lemon_squeezy/charge.rb +86 -0
- data/app/models/pay/lemon_squeezy/customer.rb +78 -0
- data/app/models/pay/lemon_squeezy/payment_method.rb +27 -0
- data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
- data/app/models/pay/merchant.rb +0 -11
- data/{lib → app/models}/pay/paddle_billing/charge.rb +2 -8
- data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +18 -35
- data/{lib → app/models}/pay/paddle_billing/payment_method.rb +2 -12
- data/{lib → app/models}/pay/paddle_billing/subscription.rb +9 -33
- data/{lib → app/models}/pay/paddle_classic/charge.rb +13 -18
- data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +9 -31
- data/{lib → app/models}/pay/paddle_classic/payment_method.rb +1 -11
- data/{lib → app/models}/pay/paddle_classic/subscription.rb +11 -36
- data/app/models/pay/payment_method.rb +0 -5
- data/{lib → app/models}/pay/stripe/charge.rb +6 -22
- data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +73 -108
- data/{lib → app/models}/pay/stripe/merchant.rb +2 -11
- data/{lib → app/models}/pay/stripe/payment_method.rb +2 -10
- data/{lib → app/models}/pay/stripe/subscription.rb +37 -71
- data/app/models/pay/subscription.rb +7 -37
- data/app/models/pay/webhook.rb +2 -0
- data/config/routes.rb +1 -0
- data/db/migrate/2_add_pay_sti_columns.rb +24 -0
- data/lib/pay/attributes.rb +11 -3
- 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 +56 -104
- data/lib/pay/paddle_billing.rb +15 -6
- data/lib/pay/paddle_classic.rb +11 -9
- data/lib/pay/receipts.rb +6 -6
- data/lib/pay/stripe/webhooks/checkout_session_completed.rb +1 -1
- data/lib/pay/stripe/webhooks/customer_updated.rb +1 -1
- data/lib/pay/stripe/webhooks/subscription_trial_will_end.rb +1 -1
- data/lib/pay/stripe.rb +21 -7
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +12 -1
- metadata +34 -38
- 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/error.rb +0 -7
@@ -1,32 +1,23 @@
|
|
1
1
|
module Pay
|
2
2
|
module FakeProcessor
|
3
|
-
class
|
4
|
-
|
3
|
+
class Customer < Pay::Customer
|
4
|
+
has_many :charges, dependent: :destroy, class_name: "Pay::FakeProcessor::Charge"
|
5
|
+
has_many :subscriptions, dependent: :destroy, class_name: "Pay::FakeProcessor::Subscription"
|
6
|
+
has_many :payment_methods, dependent: :destroy, class_name: "Pay::FakeProcessor::PaymentMethod"
|
7
|
+
has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::FakeProcessor::PaymentMethod"
|
5
8
|
|
6
|
-
|
7
|
-
:processor_id
|
8
|
-
|
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
|
-
pay_customer.update!(processor_id: NanoId.generate) unless processor_id?
|
19
|
-
pay_customer
|
9
|
+
def api_record
|
10
|
+
update!(processor_id: NanoId.generate) unless processor_id?
|
11
|
+
self
|
20
12
|
end
|
21
13
|
|
22
|
-
def
|
23
|
-
|
24
|
-
customer
|
14
|
+
def update_api_record(**attributes)
|
15
|
+
self
|
25
16
|
end
|
26
17
|
|
27
18
|
def charge(amount, options = {})
|
28
19
|
# Make to generate a processor_id
|
29
|
-
|
20
|
+
api_record
|
30
21
|
|
31
22
|
valid_attributes = options.slice(*Pay::Charge.attribute_names.map(&:to_sym))
|
32
23
|
attributes = {
|
@@ -40,12 +31,12 @@ module Pay
|
|
40
31
|
exp_year: Date.today.year
|
41
32
|
}
|
42
33
|
}.deep_merge(valid_attributes)
|
43
|
-
|
34
|
+
charges.create!(attributes)
|
44
35
|
end
|
45
36
|
|
46
37
|
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
47
38
|
# Make to generate a processor_id
|
48
|
-
|
39
|
+
api_record
|
49
40
|
attributes = options.merge(
|
50
41
|
processor_id: NanoId.generate,
|
51
42
|
name: name,
|
@@ -60,17 +51,17 @@ module Pay
|
|
60
51
|
|
61
52
|
attributes.delete(:promotion_code)
|
62
53
|
|
63
|
-
|
54
|
+
subscriptions.create!(attributes)
|
64
55
|
end
|
65
56
|
|
66
57
|
def add_payment_method(payment_method_id, default: false)
|
67
58
|
# Make to generate a processor_id
|
68
|
-
|
59
|
+
api_record
|
69
60
|
|
70
|
-
pay_payment_method =
|
61
|
+
pay_payment_method = payment_methods.create!(
|
71
62
|
processor_id: NanoId.generate,
|
72
63
|
default: default,
|
73
|
-
|
64
|
+
payment_method_type: :card,
|
74
65
|
data: {
|
75
66
|
brand: "Fake",
|
76
67
|
last4: 1234,
|
@@ -79,16 +70,12 @@ module Pay
|
|
79
70
|
}
|
80
71
|
)
|
81
72
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
def processor_subscription(subscription_id, options = {})
|
87
|
-
pay_customer.subscriptions.find_by(processor_id: subscription_id)
|
88
|
-
end
|
73
|
+
if default
|
74
|
+
payment_methods.where.not(id: pay_payment_method.id).update_all(default: false)
|
75
|
+
reload_default_payment_method
|
76
|
+
end
|
89
77
|
|
90
|
-
|
91
|
-
Date.today
|
78
|
+
pay_payment_method
|
92
79
|
end
|
93
80
|
end
|
94
81
|
end
|
@@ -1,16 +1,9 @@
|
|
1
1
|
module Pay
|
2
2
|
module FakeProcessor
|
3
|
-
class Merchant
|
4
|
-
attr_reader :pay_merchant
|
5
|
-
delegate :processor_id, to: :pay_merchant
|
6
|
-
|
7
|
-
def initialize(pay_merchant)
|
8
|
-
@pay_merchant = pay_merchant
|
9
|
-
end
|
10
|
-
|
3
|
+
class Merchant < Pay::Merchant
|
11
4
|
def create_account(**options)
|
12
5
|
fake_account = Struct.new(:id).new("fake_account_id")
|
13
|
-
|
6
|
+
update(processor_id: fake_account.id)
|
14
7
|
fake_account
|
15
8
|
end
|
16
9
|
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Pay
|
2
|
+
module FakeProcessor
|
3
|
+
class Subscription < Pay::Subscription
|
4
|
+
def api_record(**options)
|
5
|
+
self
|
6
|
+
end
|
7
|
+
|
8
|
+
# With trial, sets end to trial end (mimicing Stripe)
|
9
|
+
# Without trial, sets can ends_at to end of month
|
10
|
+
def cancel(**options)
|
11
|
+
return if canceled?
|
12
|
+
update(ends_at: (on_trial? ? trial_ends_at : Time.current.end_of_month))
|
13
|
+
end
|
14
|
+
|
15
|
+
def cancel_now!(**options)
|
16
|
+
return if canceled?
|
17
|
+
|
18
|
+
ends_at = Time.current
|
19
|
+
update(
|
20
|
+
status: :canceled,
|
21
|
+
trial_ends_at: (ends_at if trial_ends_at?),
|
22
|
+
ends_at: ends_at
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def paused?
|
27
|
+
status == "paused"
|
28
|
+
end
|
29
|
+
|
30
|
+
def pause
|
31
|
+
update(status: :paused, trial_ends_at: Time.current)
|
32
|
+
end
|
33
|
+
|
34
|
+
def resumable?
|
35
|
+
on_grace_period? || paused?
|
36
|
+
end
|
37
|
+
|
38
|
+
def resume
|
39
|
+
unless resumable?
|
40
|
+
raise StandardError, "You can only resume subscriptions within their grace period."
|
41
|
+
end
|
42
|
+
|
43
|
+
update(status: :active, trial_ends_at: nil, ends_at: nil)
|
44
|
+
end
|
45
|
+
|
46
|
+
def swap(plan, **options)
|
47
|
+
update(processor_plan: plan, ends_at: nil, status: :active)
|
48
|
+
end
|
49
|
+
|
50
|
+
def change_quantity(quantity, **options)
|
51
|
+
update(quantity: quantity)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Retries the latest invoice for a Past Due subscription
|
55
|
+
def retry_failed_payment
|
56
|
+
update(status: :active)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Pay
|
2
|
+
module LemonSqueezy
|
3
|
+
class Charge < Pay::Charge
|
4
|
+
# LemonSqueezy uses Order for one-time payments and Order + Subscription + SubscriptionInvoice for subscriptions
|
5
|
+
|
6
|
+
def self.sync_order(order_id, object: nil, try: 0, retries: 1)
|
7
|
+
object ||= ::LemonSqueezy::Order.retrieve(id: order_id)
|
8
|
+
|
9
|
+
pay_customer = Pay::Customer.find_by(type: "Pay::LemonSqueezy::Customer", processor_id: object.customer_id)
|
10
|
+
return unless pay_customer
|
11
|
+
|
12
|
+
processor_id = "order:#{object.id}"
|
13
|
+
attributes = {
|
14
|
+
processor_id: processor_id,
|
15
|
+
currency: object.currency,
|
16
|
+
subtotal: object.subtotal,
|
17
|
+
tax: object.tax,
|
18
|
+
amount: object.total,
|
19
|
+
amount_refunded: object.refunded_amount,
|
20
|
+
created_at: (object.created_at ? Time.parse(object.created_at) : nil),
|
21
|
+
updated_at: (object.updated_at ? Time.parse(object.updated_at) : nil)
|
22
|
+
}
|
23
|
+
|
24
|
+
# Update or create the charge
|
25
|
+
if (pay_charge = pay_customer.charges.find_by(processor_id: processor_id))
|
26
|
+
pay_charge.with_lock do
|
27
|
+
pay_charge.update!(attributes)
|
28
|
+
end
|
29
|
+
pay_charge
|
30
|
+
else
|
31
|
+
pay_customer.charges.create!(attributes.merge(processor_id: processor_id))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.sync_subscription_invoice(subscription_invoice_id, object: nil)
|
36
|
+
# Skip loading the latest subscription invoice details from the API if we already have it
|
37
|
+
object ||= ::LemonSqueezy::SubscriptionInvoice.retrieve(id: subscription_invoice_id)
|
38
|
+
|
39
|
+
pay_customer = Pay::Customer.find_by(type: "Pay::LemonSqueezy::Customer", processor_id: object.customer_id)
|
40
|
+
return unless pay_customer
|
41
|
+
|
42
|
+
processor_id = "subscription_invoice:#{object.id}"
|
43
|
+
subscription = Pay::LemonSqueezy::Subscription.find_by(processor_id: object.subscription_id)
|
44
|
+
attributes = {
|
45
|
+
processor_id: processor_id,
|
46
|
+
currency: object.currency,
|
47
|
+
amount: object.total,
|
48
|
+
amount_refunded: object.refunded_amount,
|
49
|
+
subtotal: object.subtotal,
|
50
|
+
tax: object.tax,
|
51
|
+
subscription: subscription,
|
52
|
+
payment_method_type: ("card" if object.card_brand.present?),
|
53
|
+
brand: object.card_brand,
|
54
|
+
last4: object.card_last_four,
|
55
|
+
created_at: (object.created_at ? Time.parse(object.created_at) : nil),
|
56
|
+
updated_at: (object.updated_at ? Time.parse(object.updated_at) : nil)
|
57
|
+
}
|
58
|
+
|
59
|
+
# Update customer's payment method
|
60
|
+
Pay::LemonSqueezy::PaymentMethod.sync(pay_customer: pay_customer, attributes: object)
|
61
|
+
|
62
|
+
# Update or create the charge
|
63
|
+
if (pay_charge = pay_customer.charges.find_by(processor_id: processor_id))
|
64
|
+
pay_charge.with_lock do
|
65
|
+
pay_charge.update!(attributes)
|
66
|
+
end
|
67
|
+
pay_charge
|
68
|
+
else
|
69
|
+
pay_customer.charges.create!(attributes.merge(processor_id: processor_id))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def api_record
|
74
|
+
ls_type, ls_id = processor_id.split(":", 2)
|
75
|
+
case ls_type
|
76
|
+
when "order"
|
77
|
+
::LemonSqueezy::Order.retrieve(id: ls_id)
|
78
|
+
when "subscription_invoice"
|
79
|
+
::LemonSqueezy::SubscriptionInvoice.retrieve(id: ls_id)
|
80
|
+
end
|
81
|
+
rescue ::LemonSqueezy::Error => e
|
82
|
+
raise Pay::LemonSqueezy::Error, e
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Pay
|
2
|
+
module LemonSqueezy
|
3
|
+
class Customer < Pay::Customer
|
4
|
+
include Pay::Routing
|
5
|
+
|
6
|
+
has_many :charges, dependent: :destroy, class_name: "Pay::LemonSqueezy::Charge"
|
7
|
+
has_many :subscriptions, dependent: :destroy, class_name: "Pay::LemonSqueezy::Subscription"
|
8
|
+
has_many :payment_methods, dependent: :destroy, class_name: "Pay::LemonSqueezy::PaymentMethod"
|
9
|
+
has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::LemonSqueezy::PaymentMethod"
|
10
|
+
|
11
|
+
def api_record_attributes
|
12
|
+
{email: email, name: customer_name}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Retrieves a LemonSqueezy::Customer object
|
16
|
+
#
|
17
|
+
# Finds an existing LemonSqueezy::Customer if processor_id exists
|
18
|
+
# Creates a new LemonSqueezy::Customer using `email` and `customer_name` if empty processor_id
|
19
|
+
#
|
20
|
+
# Returns a LemonSqueezy::Customer object
|
21
|
+
def api_record
|
22
|
+
if processor_id?
|
23
|
+
::LemonSqueezy::Customer.retrieve(id: processor_id)
|
24
|
+
elsif (lsc = ::LemonSqueezy::Customer.list(store_id: Pay::LemonSqueezy.store_id, email: email).data.first)
|
25
|
+
update!(processor_id: lsc.id)
|
26
|
+
lsc
|
27
|
+
else
|
28
|
+
lsc = ::LemonSqueezy::Customer.create(store_id: Pay::LemonSqueezy.store_id, **api_record_attributes)
|
29
|
+
update!(processor_id: lsc.id)
|
30
|
+
lsc
|
31
|
+
end
|
32
|
+
rescue ::LemonSqueezy::Error => e
|
33
|
+
raise Pay::LemonSqueezy::Error, e
|
34
|
+
end
|
35
|
+
|
36
|
+
# Syncs name and email to LemonSqueezy::Customer
|
37
|
+
# You can also pass in other attributes that will be merged into the default attributes
|
38
|
+
def update_api_record(**attributes)
|
39
|
+
api_record unless processor_id?
|
40
|
+
::LemonSqueezy::Customer.update(id: processor_id, **api_record_attributes.merge(attributes))
|
41
|
+
end
|
42
|
+
|
43
|
+
def charge(amount, options = {})
|
44
|
+
raise Pay::Error, "LemonSqueezy does not support one-off charges"
|
45
|
+
end
|
46
|
+
|
47
|
+
def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_payment_method(token = nil, default: true)
|
51
|
+
end
|
52
|
+
|
53
|
+
# checkout(variant_id: "1234")
|
54
|
+
# checkout(variant_id: "1234", product_options: {redirect_url: redirect_url})
|
55
|
+
def checkout(**options)
|
56
|
+
api_record unless processor_id?
|
57
|
+
|
58
|
+
options[:store_id] = Pay::LemonSqueezy.store_id
|
59
|
+
options[:product_options] ||= {}
|
60
|
+
options[:product_options][:redirect_url] = merge_order_id_param(options.dig(:product_options, :redirect_url) || root_url)
|
61
|
+
|
62
|
+
::LemonSqueezy::Checkout.create(**options)
|
63
|
+
end
|
64
|
+
|
65
|
+
def portal_url
|
66
|
+
api_record.urls.customer_portal
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def merge_order_id_param(url)
|
72
|
+
uri = URI.parse(url)
|
73
|
+
uri.query = URI.encode_www_form(URI.decode_www_form(uri.query.to_s).to_h.merge("lemon_squeezy_order_id" => "[order_id]").to_a)
|
74
|
+
uri.to_s.gsub("%5Border_id%5D", "[order_id]")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Pay
|
2
|
+
module LemonSqueezy
|
3
|
+
class PaymentMethod < Pay::PaymentMethod
|
4
|
+
def self.sync(pay_customer:, attributes:)
|
5
|
+
return unless pay_customer.subscription
|
6
|
+
|
7
|
+
payment_method = pay_customer.default_payment_method || pay_customer.build_default_payment_method
|
8
|
+
payment_method.processor_id ||= NanoId.generate
|
9
|
+
|
10
|
+
attrs = {
|
11
|
+
payment_method_type: "card",
|
12
|
+
brand: attributes.card_brand,
|
13
|
+
last4: attributes.card_last_four
|
14
|
+
}
|
15
|
+
|
16
|
+
payment_method.update!(attrs)
|
17
|
+
payment_method
|
18
|
+
end
|
19
|
+
|
20
|
+
def make_default!
|
21
|
+
end
|
22
|
+
|
23
|
+
def detach
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -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 = pay_customer.subscriptions.find_by(processor_id: object.id))
|
37
|
+
pay_subscription.with_lock do
|
38
|
+
pay_subscription.update!(attributes)
|
39
|
+
end
|
40
|
+
pay_subscription
|
41
|
+
else
|
42
|
+
pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.id))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def api_record(**options)
|
47
|
+
@api_record ||= ::LemonSqueezy::Subscription.retrieve(id: processor_id)
|
48
|
+
rescue ::LemonSqueezy::Error => e
|
49
|
+
raise Pay::LemonSqueezy::Error, e
|
50
|
+
end
|
51
|
+
|
52
|
+
def portal_url
|
53
|
+
api_record.urls.customer_portal
|
54
|
+
end
|
55
|
+
|
56
|
+
def update_url
|
57
|
+
api_record.urls.update_payment_method
|
58
|
+
end
|
59
|
+
|
60
|
+
def cancel(**options)
|
61
|
+
return if canceled?
|
62
|
+
response = ::LemonSqueezy::Subscription.cancel(id: processor_id)
|
63
|
+
update(status: response.status, ends_at: response.ends_at)
|
64
|
+
rescue ::LemonSqueezy::Error => e
|
65
|
+
raise Pay::LemonSqueezy::Error, e
|
66
|
+
end
|
67
|
+
|
68
|
+
def cancel_now!(**options)
|
69
|
+
raise Pay::Error, "Lemon Squeezy does not support cancelling immediately through the API."
|
70
|
+
end
|
71
|
+
|
72
|
+
def change_quantity(quantity, **options)
|
73
|
+
subscription_item = api_record.first_subscription_item
|
74
|
+
::LemonSqueezy::SubscriptionItem.update(id: subscription_item.id, quantity: quantity)
|
75
|
+
update(quantity: quantity)
|
76
|
+
rescue ::LemonSqueezy::Error => e
|
77
|
+
raise Pay::LemonSqueezy::Error, e
|
78
|
+
end
|
79
|
+
|
80
|
+
# A subscription could be set to cancel or pause in the future
|
81
|
+
# It is considered on grace period until the cancel or pause time begins
|
82
|
+
def on_grace_period?
|
83
|
+
(canceled? && Time.current < ends_at) || (paused? && pause_starts_at? && Time.current < pause_starts_at)
|
84
|
+
end
|
85
|
+
|
86
|
+
def paused?
|
87
|
+
status == "paused"
|
88
|
+
end
|
89
|
+
|
90
|
+
def pause(**options)
|
91
|
+
response = ::LemonSqueezy::Subscription.pause(id: processor_id, **options)
|
92
|
+
update!(status: :paused, pause_starts_at: response.pause&.resumes_at)
|
93
|
+
rescue ::LemonSqueezy::Error => e
|
94
|
+
raise Pay::LemonSqueezy::Error, e
|
95
|
+
end
|
96
|
+
|
97
|
+
def resumable?
|
98
|
+
paused? || canceled?
|
99
|
+
end
|
100
|
+
|
101
|
+
def resume
|
102
|
+
unless resumable?
|
103
|
+
raise StandardError, "You can only resume paused or cancelled subscriptions"
|
104
|
+
end
|
105
|
+
|
106
|
+
if paused? && pause_starts_at? && Time.current < pause_starts_at
|
107
|
+
::LemonSqueezy::Subscription.unpause(id: processor_id)
|
108
|
+
else
|
109
|
+
::LemonSqueezy::Subscription.uncancel(id: processor_id)
|
110
|
+
end
|
111
|
+
|
112
|
+
update(ends_at: nil, status: :active, pause_starts_at: nil)
|
113
|
+
rescue ::LemonSqueezy::Error => e
|
114
|
+
raise Pay::LemonSqueezy::Error, e
|
115
|
+
end
|
116
|
+
|
117
|
+
# Lemon Squeezy requires both the Product ID and Variant ID.
|
118
|
+
# The Variant ID will be saved as the processor_plan
|
119
|
+
def swap(plan, **options)
|
120
|
+
raise StandardError, "A plan_id is required to swap a subscription" unless plan
|
121
|
+
raise StandardError, "A variant_id is required to swap a subscription" unless options[:variant_id]
|
122
|
+
|
123
|
+
::LemonSqueezy::Subscription.change_plan id: processor_id, plan_id: plan, variant_id: options[:variant_id]
|
124
|
+
|
125
|
+
update(processor_plan: options[:variant_id], ends_at: nil, status: :active)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/app/models/pay/merchant.rb
CHANGED
@@ -6,17 +6,6 @@ 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
|
@@ -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
|
@@ -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: sc.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,18 +66,7 @@ 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
|
@@ -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"
|
@@ -15,7 +11,7 @@ module Pay
|
|
15
11
|
def self.sync(pay_customer:, attributes:)
|
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
|
@@ -31,15 +27,9 @@ module Pay
|
|
31
27
|
payment_method
|
32
28
|
end
|
33
29
|
|
34
|
-
def initialize(pay_payment_method)
|
35
|
-
@pay_payment_method = pay_payment_method
|
36
|
-
end
|
37
|
-
|
38
|
-
# Sets payment method as default
|
39
30
|
def make_default!
|
40
31
|
end
|
41
32
|
|
42
|
-
# Remove payment method
|
43
33
|
def detach
|
44
34
|
end
|
45
35
|
end
|