pay 7.3.0 → 8.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/app/controllers/pay/webhooks/lemon_squeezy_controller.rb +45 -0
  4. data/app/jobs/pay/customer_sync_job.rb +1 -1
  5. data/app/models/concerns/pay/routing.rb +13 -0
  6. data/{lib → app/models}/pay/braintree/charge.rb +5 -12
  7. data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +31 -71
  8. data/{lib → app/models}/pay/braintree/payment_method.rb +1 -9
  9. data/{lib → app/models}/pay/braintree/subscription.rb +15 -53
  10. data/app/models/pay/charge.rb +8 -27
  11. data/app/models/pay/customer.rb +2 -15
  12. data/app/models/pay/fake_processor/charge.rb +13 -0
  13. data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +20 -37
  14. data/{lib → app/models}/pay/fake_processor/merchant.rb +2 -9
  15. data/app/models/pay/fake_processor/payment_method.rb +11 -0
  16. data/app/models/pay/fake_processor/subscription.rb +60 -0
  17. data/app/models/pay/lemon_squeezy/charge.rb +86 -0
  18. data/app/models/pay/lemon_squeezy/customer.rb +78 -0
  19. data/app/models/pay/lemon_squeezy/payment_method.rb +27 -0
  20. data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
  21. data/app/models/pay/merchant.rb +0 -11
  22. data/{lib → app/models}/pay/paddle_billing/charge.rb +2 -8
  23. data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +18 -35
  24. data/{lib → app/models}/pay/paddle_billing/payment_method.rb +2 -12
  25. data/{lib → app/models}/pay/paddle_billing/subscription.rb +9 -33
  26. data/{lib → app/models}/pay/paddle_classic/charge.rb +13 -18
  27. data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +9 -31
  28. data/{lib → app/models}/pay/paddle_classic/payment_method.rb +1 -11
  29. data/{lib → app/models}/pay/paddle_classic/subscription.rb +11 -36
  30. data/app/models/pay/payment_method.rb +0 -5
  31. data/{lib → app/models}/pay/stripe/charge.rb +6 -22
  32. data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +73 -108
  33. data/{lib → app/models}/pay/stripe/merchant.rb +2 -11
  34. data/{lib → app/models}/pay/stripe/payment_method.rb +2 -10
  35. data/{lib → app/models}/pay/stripe/subscription.rb +37 -71
  36. data/app/models/pay/subscription.rb +7 -37
  37. data/app/models/pay/webhook.rb +3 -1
  38. data/config/routes.rb +1 -0
  39. data/db/migrate/2_add_pay_sti_columns.rb +24 -0
  40. data/lib/pay/attributes.rb +11 -3
  41. data/lib/pay/braintree.rb +25 -6
  42. data/lib/pay/engine.rb +2 -0
  43. data/lib/pay/fake_processor.rb +2 -6
  44. data/lib/pay/lemon_squeezy/webhooks/order.rb +11 -0
  45. data/lib/pay/lemon_squeezy/webhooks/subscription.rb +3 -3
  46. data/lib/pay/lemon_squeezy/webhooks/subscription_payment.rb +11 -0
  47. data/lib/pay/lemon_squeezy.rb +56 -104
  48. data/lib/pay/paddle_billing.rb +15 -6
  49. data/lib/pay/paddle_classic.rb +11 -9
  50. data/lib/pay/receipts.rb +6 -6
  51. data/lib/pay/stripe/webhooks/customer_updated.rb +1 -1
  52. data/lib/pay/stripe.rb +16 -7
  53. data/lib/pay/version.rb +1 -1
  54. data/lib/pay.rb +12 -1
  55. metadata +34 -38
  56. data/app/views/pay/stripe/_checkout_button.html.erb +0 -21
  57. data/lib/pay/braintree/authorization_error.rb +0 -9
  58. data/lib/pay/braintree/error.rb +0 -23
  59. data/lib/pay/fake_processor/charge.rb +0 -21
  60. data/lib/pay/fake_processor/error.rb +0 -6
  61. data/lib/pay/fake_processor/payment_method.rb +0 -21
  62. data/lib/pay/fake_processor/subscription.rb +0 -90
  63. data/lib/pay/lemon_squeezy/billable.rb +0 -90
  64. data/lib/pay/lemon_squeezy/charge.rb +0 -68
  65. data/lib/pay/lemon_squeezy/error.rb +0 -7
  66. data/lib/pay/lemon_squeezy/payment_method.rb +0 -40
  67. data/lib/pay/lemon_squeezy/subscription.rb +0 -185
  68. data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +0 -11
  69. data/lib/pay/paddle_billing/error.rb +0 -7
  70. data/lib/pay/paddle_classic/error.rb +0 -7
  71. data/lib/pay/stripe/error.rb +0 -7
@@ -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,
@@ -60,17 +51,17 @@ module Pay
60
51
 
61
52
  attributes.delete(:promotion_code)
62
53
 
63
- pay_customer.subscriptions.create!(attributes)
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
- customer
59
+ api_record
69
60
 
70
- pay_payment_method = pay_customer.payment_methods.create!(
61
+ pay_payment_method = payment_methods.create!(
71
62
  processor_id: NanoId.generate,
72
63
  default: default,
73
- type: :card,
64
+ payment_method_type: :card,
74
65
  data: {
75
66
  brand: "Fake",
76
67
  last4: 1234,
@@ -80,20 +71,12 @@ module Pay
80
71
  )
81
72
 
82
73
  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
74
+ payment_methods.where.not(id: pay_payment_method.id).update_all(default: false)
75
+ reload_default_payment_method
85
76
  end
86
77
 
87
78
  pay_payment_method
88
79
  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
80
  end
98
81
  end
99
82
  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
- pay_merchant.update(processor_id: fake_account.id)
6
+ update(processor_id: fake_account.id)
14
7
  fake_account
15
8
  end
16
9
 
@@ -0,0 +1,11 @@
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
@@ -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
@@ -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
- attr_reader :pay_charge
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 Billable
4
- attr_reader :pay_customer
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
- 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_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 customer
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
- sc = ::Paddle::Customer.create(email: email, name: customer_name)
32
- pay_customer.update!(processor_id: sc.id)
33
- sc
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 update_customer!(**attributes)
42
- customer unless processor_id?
43
- attrs = customer_attributes.merge(attributes)
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 = pay_customer.charges.find_or_initialize_by(processor_id: transaction.id)
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: 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
- type: details.type.downcase
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