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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -4
  3. data/app/controllers/pay/payments_controller.rb +2 -0
  4. data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +45 -0
  5. data/app/jobs/pay/customer_sync_job.rb +1 -1
  6. data/app/models/concerns/pay/routing.rb +13 -0
  7. data/{lib → app/models}/pay/braintree/charge.rb +7 -12
  8. data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +33 -71
  9. data/{lib → app/models}/pay/braintree/payment_method.rb +4 -10
  10. data/{lib → app/models}/pay/braintree/subscription.rb +23 -61
  11. data/app/models/pay/charge.rb +16 -45
  12. data/app/models/pay/customer.rb +5 -16
  13. data/app/models/pay/fake_processor/charge.rb +19 -0
  14. data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +28 -38
  15. data/{lib → app/models}/pay/fake_processor/merchant.rb +4 -9
  16. data/app/models/pay/fake_processor/payment_method.rb +13 -0
  17. data/app/models/pay/fake_processor/subscription.rb +70 -0
  18. data/app/models/pay/lemon_squeezy/charge.rb +96 -0
  19. data/app/models/pay/lemon_squeezy/customer.rb +80 -0
  20. data/app/models/pay/lemon_squeezy/payment_method.rb +29 -0
  21. data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
  22. data/app/models/pay/merchant.rb +2 -11
  23. data/{lib → app/models}/pay/paddle_billing/charge.rb +15 -13
  24. data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +20 -35
  25. data/{lib → app/models}/pay/paddle_billing/payment_method.rb +13 -13
  26. data/{lib → app/models}/pay/paddle_billing/subscription.rb +40 -43
  27. data/{lib → app/models}/pay/paddle_classic/charge.rb +15 -18
  28. data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +11 -31
  29. data/{lib → app/models}/pay/paddle_classic/payment_method.rb +3 -11
  30. data/{lib → app/models}/pay/paddle_classic/subscription.rb +17 -37
  31. data/app/models/pay/payment_method.rb +4 -5
  32. data/app/models/pay/stripe/charge.rb +155 -0
  33. data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +78 -111
  34. data/{lib → app/models}/pay/stripe/merchant.rb +5 -20
  35. data/{lib → app/models}/pay/stripe/payment_method.rb +11 -17
  36. data/{lib → app/models}/pay/stripe/subscription.rb +83 -112
  37. data/app/models/pay/subscription.rb +13 -47
  38. data/app/models/pay/webhook.rb +5 -1
  39. data/app/views/pay/user_mailer/payment_action_required.text.erb +9 -0
  40. data/app/views/pay/user_mailer/payment_failed.text.erb +9 -0
  41. data/app/views/pay/user_mailer/receipt.text.erb +20 -0
  42. data/app/views/pay/user_mailer/refund.text.erb +21 -0
  43. data/app/views/pay/user_mailer/subscription_renewing.text.erb +8 -0
  44. data/app/views/pay/user_mailer/subscription_trial_ended.text.erb +8 -0
  45. data/app/views/pay/user_mailer/subscription_trial_will_end.text.erb +8 -0
  46. data/config/locales/en.yml +1 -0
  47. data/config/routes.rb +1 -0
  48. data/db/migrate/20250415151129_add_object_to_pay_models.rb +7 -0
  49. data/db/migrate/2_add_pay_sti_columns.rb +24 -0
  50. data/lib/pay/attributes.rb +16 -8
  51. data/lib/pay/braintree.rb +25 -6
  52. data/lib/pay/engine.rb +2 -0
  53. data/lib/pay/fake_processor.rb +2 -6
  54. data/lib/pay/lemon_squeezy/webhooks/order.rb +11 -0
  55. data/lib/pay/lemon_squeezy/webhooks/subscription.rb +3 -3
  56. data/lib/pay/lemon_squeezy/webhooks/subscription_payment.rb +11 -0
  57. data/lib/pay/lemon_squeezy.rb +58 -104
  58. data/lib/pay/nano_id.rb +1 -1
  59. data/lib/pay/paddle_billing.rb +15 -6
  60. data/lib/pay/paddle_classic/webhooks/signature_verifier.rb +1 -1
  61. data/lib/pay/paddle_classic.rb +11 -9
  62. data/lib/pay/receipts.rb +45 -44
  63. data/lib/pay/stripe/webhooks/charge_updated.rb +11 -0
  64. data/lib/pay/stripe/webhooks/customer_updated.rb +13 -9
  65. data/lib/pay/stripe/webhooks/payment_action_required.rb +10 -6
  66. data/lib/pay/stripe/webhooks/payment_failed.rb +6 -4
  67. data/lib/pay/stripe/webhooks/subscription_renewing.rb +9 -4
  68. data/lib/pay/stripe.rb +28 -9
  69. data/lib/pay/version.rb +1 -1
  70. data/lib/pay.rb +19 -1
  71. data/lib/tasks/pay.rake +2 -2
  72. metadata +45 -43
  73. data/app/views/pay/stripe/_checkout_button.html.erb +0 -21
  74. data/lib/pay/braintree/authorization_error.rb +0 -9
  75. data/lib/pay/braintree/error.rb +0 -23
  76. data/lib/pay/fake_processor/charge.rb +0 -21
  77. data/lib/pay/fake_processor/error.rb +0 -6
  78. data/lib/pay/fake_processor/payment_method.rb +0 -21
  79. data/lib/pay/fake_processor/subscription.rb +0 -90
  80. data/lib/pay/lemon_squeezy/billable.rb +0 -90
  81. data/lib/pay/lemon_squeezy/charge.rb +0 -68
  82. data/lib/pay/lemon_squeezy/error.rb +0 -7
  83. data/lib/pay/lemon_squeezy/payment_method.rb +0 -40
  84. data/lib/pay/lemon_squeezy/subscription.rb +0 -185
  85. data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +0 -11
  86. data/lib/pay/paddle_billing/error.rb +0 -7
  87. data/lib/pay/paddle_classic/error.rb +0 -7
  88. data/lib/pay/stripe/charge.rb +0 -176
  89. data/lib/pay/stripe/error.rb +0 -7
@@ -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
- # Store the payment method kind (card, paypal, etc)
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, :amount_captured
33
- store_accessor :data, :invoice_id
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 self.pay_processor_for(name)
62
- "Pay::#{name.to_s.classify}::Charge".constantize
63
- end
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
- "#{brand} (#{email})"
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
@@ -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
- query = {name: name, processor_plan: processor_plan}.compact
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 Billable
4
- attr_reader :pay_customer
5
-
6
- delegate :processor_id,
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
- 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 update_customer!(**attributes)
23
- # return customer to fake an update
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
- customer
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
- pay_customer.charges.create!(attributes)
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
- customer
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
- attributes.delete(:promotion_code)
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
- pay_customer.subscriptions.create!(attributes)
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
- customer
64
+ api_record
69
65
 
70
- pay_payment_method = pay_customer.payment_methods.create!(
66
+ pay_payment_method = payment_methods.create!(
71
67
  processor_id: NanoId.generate,
72
68
  default: default,
73
- type: :card,
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
- pay_customer.payment_methods.where.not(id: pay_payment_method.id).update_all(default: false)
84
- pay_customer.reload_default_payment_method
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
- pay_merchant.update(processor_id: fake_account.id)
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,13 @@
1
+ module Pay
2
+ module FakeProcessor
3
+ class PaymentMethod < Pay::PaymentMethod
4
+ def make_default!
5
+ end
6
+
7
+ def detach
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ ActiveSupport.run_load_hooks :pay_fake_processor_payment_method, Pay::FakeProcessor::PaymentMethod
@@ -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