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
data/app/models/pay/charge.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
module Pay
|
2
2
|
class Charge < Pay::ApplicationRecord
|
3
|
-
self.inheritance_column = nil
|
4
|
-
|
5
3
|
# Associations
|
6
4
|
belongs_to :customer
|
7
5
|
belongs_to :subscription, optional: true
|
@@ -15,9 +13,7 @@ module Pay
|
|
15
13
|
validates :amount, presence: true
|
16
14
|
validates :processor_id, presence: true, uniqueness: {scope: :customer_id, case_sensitive: true}
|
17
15
|
|
18
|
-
|
19
|
-
store_accessor :data, :paddle_receipt_url
|
20
|
-
store_accessor :data, :stripe_receipt_url
|
16
|
+
delegate :owner, to: :customer
|
21
17
|
|
22
18
|
# Payment method attributes
|
23
19
|
store_accessor :data, :payment_method_type # card, paypal, sepa, etc
|
@@ -29,22 +25,11 @@ module Pay
|
|
29
25
|
store_accessor :data, :username # Venmo
|
30
26
|
store_accessor :data, :bank
|
31
27
|
|
32
|
-
store_accessor :data, :
|
33
|
-
store_accessor :data, :
|
34
|
-
store_accessor :data, :payment_intent_id
|
35
|
-
store_accessor :data, :period_start
|
36
|
-
store_accessor :data, :period_end
|
37
|
-
store_accessor :data, :line_items
|
38
|
-
store_accessor :data, :subtotal # subtotal amount in cents
|
39
|
-
store_accessor :data, :tax # total tax amount in cents
|
40
|
-
store_accessor :data, :discounts # array of discount IDs applied to the Stripe Invoice
|
41
|
-
store_accessor :data, :total_discount_amounts # array of discount details
|
42
|
-
store_accessor :data, :total_tax_amounts # array of tax details for each jurisdiction
|
43
|
-
store_accessor :data, :credit_notes # array of credit notes for the Stripe Invoice
|
44
|
-
store_accessor :data, :refunds # array of refunds
|
28
|
+
store_accessor :data, :subtotal
|
29
|
+
store_accessor :data, :tax
|
45
30
|
|
46
31
|
# Helpers for payment processors
|
47
|
-
%w[braintree stripe paddle_billing paddle_classic fake_processor].each do |processor_name|
|
32
|
+
%w[braintree stripe paddle_billing paddle_classic lemon_squeezy fake_processor].each do |processor_name|
|
48
33
|
define_method :"#{processor_name}?" do
|
49
34
|
customer.processor == processor_name
|
50
35
|
end
|
@@ -52,31 +37,13 @@ module Pay
|
|
52
37
|
scope processor_name, -> { joins(:customer).where(pay_customers: {processor: processor_name}) }
|
53
38
|
end
|
54
39
|
|
55
|
-
delegate :capture, :credit_note!, :credit_notes, to: :payment_processor
|
56
|
-
|
57
40
|
def self.find_by_processor_and_id(processor, processor_id)
|
58
41
|
joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
|
59
42
|
end
|
60
43
|
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
def payment_processor
|
66
|
-
@payment_processor ||= self.class.pay_processor_for(customer.processor).new(self)
|
67
|
-
end
|
68
|
-
|
69
|
-
def processor_charge
|
70
|
-
payment_processor.charge
|
71
|
-
end
|
72
|
-
|
73
|
-
def captured?
|
74
|
-
amount_captured > 0
|
75
|
-
end
|
76
|
-
|
77
|
-
def refund!(refund_amount = nil)
|
78
|
-
refund_amount ||= amount
|
79
|
-
payment_processor.refund!(refund_amount)
|
44
|
+
def sync!(**options)
|
45
|
+
self.class.sync(processor_id, **options)
|
46
|
+
reload
|
80
47
|
end
|
81
48
|
|
82
49
|
def refunded?
|
@@ -104,7 +71,13 @@ module Pay
|
|
104
71
|
when "card"
|
105
72
|
"#{brand.titleize} (**** **** **** #{last4})"
|
106
73
|
when "paypal"
|
107
|
-
|
74
|
+
# Sometimes brand and email are missing (Stripe)
|
75
|
+
brand ||= "PayPal"
|
76
|
+
if email.present?
|
77
|
+
brand + " (#{email})"
|
78
|
+
else
|
79
|
+
brand
|
80
|
+
end
|
108
81
|
|
109
82
|
# Braintree
|
110
83
|
when "venmo"
|
@@ -131,9 +104,7 @@ module Pay
|
|
131
104
|
payment_method_type&.titleize
|
132
105
|
end
|
133
106
|
end
|
134
|
-
|
135
|
-
def line_items
|
136
|
-
Array.wrap(super)
|
137
|
-
end
|
138
107
|
end
|
139
108
|
end
|
109
|
+
|
110
|
+
ActiveSupport.run_load_hooks :pay_charge, Pay::Charge
|
data/app/models/pay/customer.rb
CHANGED
@@ -13,8 +13,6 @@ module Pay
|
|
13
13
|
validates :processor, presence: true
|
14
14
|
validates :processor_id, allow_blank: true, uniqueness: {scope: :processor, case_sensitive: true}
|
15
15
|
|
16
|
-
attribute :payment_method_token, :string
|
17
|
-
|
18
16
|
# Account(s) for marketplace payments
|
19
17
|
store_accessor :data, :braintree_account
|
20
18
|
|
@@ -22,10 +20,9 @@ module Pay
|
|
22
20
|
store_accessor :data, :invoice_credit_balance
|
23
21
|
store_accessor :data, :currency
|
24
22
|
|
25
|
-
delegate :email, to: :owner
|
26
|
-
delegate_missing_to :pay_processor
|
23
|
+
delegate :email, to: :owner, allow_nil: true
|
27
24
|
|
28
|
-
%w[stripe braintree paddle_billing paddle_classic fake_processor].each do |processor_name|
|
25
|
+
%w[stripe braintree paddle_billing paddle_classic lemon_squeezy fake_processor].each do |processor_name|
|
29
26
|
scope processor_name, -> { where(processor: processor_name) }
|
30
27
|
|
31
28
|
define_method :"#{processor_name}?" do
|
@@ -33,15 +30,6 @@ module Pay
|
|
33
30
|
end
|
34
31
|
end
|
35
32
|
|
36
|
-
def self.pay_processor_for(name)
|
37
|
-
"Pay::#{name.to_s.classify}::Billable".constantize
|
38
|
-
end
|
39
|
-
|
40
|
-
def pay_processor
|
41
|
-
return if processor.blank?
|
42
|
-
@pay_processor ||= self.class.pay_processor_for(processor).new(self)
|
43
|
-
end
|
44
|
-
|
45
33
|
def update_payment_method(payment_method_id)
|
46
34
|
add_payment_method(payment_method_id, default: true)
|
47
35
|
end
|
@@ -51,8 +39,7 @@ module Pay
|
|
51
39
|
end
|
52
40
|
|
53
41
|
def subscribed?(name: Pay.default_product_name, processor_plan: nil)
|
54
|
-
|
55
|
-
subscriptions.active.where(query).exists?
|
42
|
+
subscriptions.active.where({name: name, processor_plan: processor_plan}.compact).exists?
|
56
43
|
end
|
57
44
|
|
58
45
|
def on_trial?(name: Pay.default_product_name, plan: nil)
|
@@ -101,3 +88,5 @@ module Pay
|
|
101
88
|
end
|
102
89
|
end
|
103
90
|
end
|
91
|
+
|
92
|
+
ActiveSupport.run_load_hooks :pay_customer, Pay::Customer
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Pay
|
2
|
+
module FakeProcessor
|
3
|
+
class Charge < Pay::Charge
|
4
|
+
def self.sync(processor_id)
|
5
|
+
true
|
6
|
+
end
|
7
|
+
|
8
|
+
def api_record
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def refund!(amount_to_refund = nil)
|
13
|
+
update(amount_refunded: amount_to_refund || amount)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ActiveSupport.run_load_hooks :pay_fake_processor_charge, Pay::FakeProcessor::Charge
|
@@ -1,32 +1,23 @@
|
|
1
1
|
module Pay
|
2
2
|
module FakeProcessor
|
3
|
-
class
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
:
|
11
|
-
|
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
|
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"
|
8
|
+
|
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,
|
@@ -58,19 +49,24 @@ module Pay
|
|
58
49
|
attributes[:trial_ends_at] = trial_period_days.to_i.days.from_now
|
59
50
|
end
|
60
51
|
|
61
|
-
|
52
|
+
# Ignore any keys that aren't attribute names
|
53
|
+
attributes.deep_stringify_keys!.slice!(*subscriptions.attribute_names)
|
54
|
+
|
55
|
+
subscriptions.create!(attributes)
|
56
|
+
end
|
62
57
|
|
63
|
-
|
58
|
+
def sync_subscriptions(**options)
|
59
|
+
[]
|
64
60
|
end
|
65
61
|
|
66
62
|
def add_payment_method(payment_method_id, default: false)
|
67
63
|
# Make to generate a processor_id
|
68
|
-
|
64
|
+
api_record
|
69
65
|
|
70
|
-
pay_payment_method =
|
66
|
+
pay_payment_method = payment_methods.create!(
|
71
67
|
processor_id: NanoId.generate,
|
72
68
|
default: default,
|
73
|
-
|
69
|
+
payment_method_type: :card,
|
74
70
|
data: {
|
75
71
|
brand: "Fake",
|
76
72
|
last4: 1234,
|
@@ -80,20 +76,14 @@ module Pay
|
|
80
76
|
)
|
81
77
|
|
82
78
|
if default
|
83
|
-
|
84
|
-
|
79
|
+
payment_methods.where.not(id: pay_payment_method.id).update_all(default: false)
|
80
|
+
reload_default_payment_method
|
85
81
|
end
|
86
82
|
|
87
83
|
pay_payment_method
|
88
84
|
end
|
89
|
-
|
90
|
-
def processor_subscription(subscription_id, options = {})
|
91
|
-
pay_customer.subscriptions.find_by(processor_id: subscription_id)
|
92
|
-
end
|
93
|
-
|
94
|
-
def trial_end_date(subscription)
|
95
|
-
Date.today
|
96
|
-
end
|
97
85
|
end
|
98
86
|
end
|
99
87
|
end
|
88
|
+
|
89
|
+
ActiveSupport.run_load_hooks :pay_fake_processor_customer, Pay::FakeProcessor::Customer
|
@@ -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
|
|
@@ -24,3 +17,5 @@ module Pay
|
|
24
17
|
end
|
25
18
|
end
|
26
19
|
end
|
20
|
+
|
21
|
+
ActiveSupport.run_load_hooks :pay_fake_processor_merchant, Pay::FakeProcessor::Merchant
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Pay
|
2
|
+
module FakeProcessor
|
3
|
+
class Subscription < Pay::Subscription
|
4
|
+
def self.sync(processor_id, **options)
|
5
|
+
# Bypass sync operation for FakeProcessor
|
6
|
+
end
|
7
|
+
|
8
|
+
def api_record(**options)
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
# With trial, sets end to trial end (mimicking Stripe)
|
13
|
+
# Without trial, sets can ends_at to end of month
|
14
|
+
def cancel(**options)
|
15
|
+
return if canceled?
|
16
|
+
update(ends_at: (on_trial? ? trial_ends_at : Time.current.end_of_month))
|
17
|
+
end
|
18
|
+
|
19
|
+
def cancel_now!(**options)
|
20
|
+
return if canceled?
|
21
|
+
|
22
|
+
ends_at = Time.current
|
23
|
+
update(
|
24
|
+
status: :canceled,
|
25
|
+
trial_ends_at: (ends_at if trial_ends_at?),
|
26
|
+
ends_at: ends_at
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def paused?
|
31
|
+
status == "paused"
|
32
|
+
end
|
33
|
+
|
34
|
+
def pause
|
35
|
+
update(status: :paused, trial_ends_at: Time.current)
|
36
|
+
end
|
37
|
+
|
38
|
+
def resumable?
|
39
|
+
if data&.has_key?("resumable")
|
40
|
+
data["resumable"]
|
41
|
+
else
|
42
|
+
on_grace_period? || paused?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def resume
|
47
|
+
unless resumable?
|
48
|
+
raise Error, "You can only resume subscriptions within their grace period."
|
49
|
+
end
|
50
|
+
|
51
|
+
update(status: :active, trial_ends_at: nil, ends_at: nil)
|
52
|
+
end
|
53
|
+
|
54
|
+
def swap(plan, **options)
|
55
|
+
update(processor_plan: plan, ends_at: nil, status: :active)
|
56
|
+
end
|
57
|
+
|
58
|
+
def change_quantity(quantity, **options)
|
59
|
+
update(quantity: quantity)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Retries the latest invoice for a Past Due subscription
|
63
|
+
def retry_failed_payment
|
64
|
+
update(status: :active)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
ActiveSupport.run_load_hooks :pay_fake_processor_subscription, Pay::FakeProcessor::Subscription
|
@@ -0,0 +1,96 @@
|
|
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 = find_by(customer: pay_customer, processor_id: processor_id))
|
26
|
+
pay_charge.with_lock { pay_charge.update!(attributes) }
|
27
|
+
pay_charge
|
28
|
+
else
|
29
|
+
create!(attributes.merge(customer: pay_customer, processor_id: processor_id))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.sync_subscription_invoice(subscription_invoice_id, object: nil)
|
34
|
+
# Skip loading the latest subscription invoice details from the API if we already have it
|
35
|
+
object ||= ::LemonSqueezy::SubscriptionInvoice.retrieve(id: subscription_invoice_id)
|
36
|
+
|
37
|
+
pay_customer = Pay::Customer.find_by(type: "Pay::LemonSqueezy::Customer", processor_id: object.customer_id)
|
38
|
+
return unless pay_customer
|
39
|
+
|
40
|
+
processor_id = "subscription_invoice:#{object.id}"
|
41
|
+
subscription = Pay::LemonSqueezy::Subscription.find_by(processor_id: object.subscription_id)
|
42
|
+
attributes = {
|
43
|
+
processor_id: processor_id,
|
44
|
+
currency: object.currency,
|
45
|
+
amount: object.total,
|
46
|
+
amount_refunded: object.refunded_amount,
|
47
|
+
subtotal: object.subtotal,
|
48
|
+
tax: object.tax,
|
49
|
+
subscription: subscription,
|
50
|
+
payment_method_type: ("card" if object.card_brand.present?),
|
51
|
+
brand: object.card_brand,
|
52
|
+
last4: object.card_last_four,
|
53
|
+
created_at: (object.created_at ? Time.parse(object.created_at) : nil),
|
54
|
+
updated_at: (object.updated_at ? Time.parse(object.updated_at) : nil)
|
55
|
+
}
|
56
|
+
|
57
|
+
# Update customer's payment method
|
58
|
+
Pay::LemonSqueezy::PaymentMethod.sync(pay_customer: pay_customer, attributes: object)
|
59
|
+
|
60
|
+
# Update or create the charge
|
61
|
+
if (pay_charge = pay_customer.charges.find_by(processor_id: processor_id))
|
62
|
+
pay_charge.with_lock do
|
63
|
+
pay_charge.update!(attributes)
|
64
|
+
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
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def api_record
|
82
|
+
ls_type, ls_id = processor_id.split(":", 2)
|
83
|
+
case ls_type
|
84
|
+
when "order"
|
85
|
+
::LemonSqueezy::Order.retrieve(id: ls_id)
|
86
|
+
when "subscription_invoice"
|
87
|
+
::LemonSqueezy::SubscriptionInvoice.retrieve(id: ls_id)
|
88
|
+
end
|
89
|
+
rescue ::LemonSqueezy::Error => e
|
90
|
+
raise Pay::LemonSqueezy::Error, e
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
ActiveSupport.run_load_hooks :pay_lemon_squeezy_charge, Pay::LemonSqueezy::Charge
|
@@ -0,0 +1,80 @@
|
|
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
|
79
|
+
|
80
|
+
ActiveSupport.run_load_hooks :pay_lemon_squeezy_customer, Pay::LemonSqueezy::Customer
|
@@ -0,0 +1,29 @@
|
|
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
|
28
|
+
|
29
|
+
ActiveSupport.run_load_hooks :pay_lemon_squeezy_payment_method, Pay::LemonSqueezy::PaymentMethod
|