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.
Files changed (74) 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/controllers/pay/webhooks/stripe_controller.rb +2 -1
  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 +5 -12
  8. data/{lib/pay/braintree/billable.rb → app/models/pay/braintree/customer.rb} +31 -71
  9. data/{lib → app/models}/pay/braintree/payment_method.rb +1 -9
  10. data/{lib → app/models}/pay/braintree/subscription.rb +14 -52
  11. data/app/models/pay/charge.rb +8 -27
  12. data/app/models/pay/customer.rb +2 -15
  13. data/app/models/pay/fake_processor/charge.rb +13 -0
  14. data/{lib/pay/fake_processor/billable.rb → app/models/pay/fake_processor/customer.rb} +22 -35
  15. data/{lib → app/models}/pay/fake_processor/merchant.rb +2 -9
  16. data/app/models/pay/fake_processor/payment_method.rb +11 -0
  17. data/app/models/pay/fake_processor/subscription.rb +60 -0
  18. data/app/models/pay/lemon_squeezy/charge.rb +86 -0
  19. data/app/models/pay/lemon_squeezy/customer.rb +78 -0
  20. data/app/models/pay/lemon_squeezy/payment_method.rb +27 -0
  21. data/app/models/pay/lemon_squeezy/subscription.rb +129 -0
  22. data/app/models/pay/merchant.rb +0 -11
  23. data/{lib → app/models}/pay/paddle_billing/charge.rb +2 -8
  24. data/{lib/pay/paddle_billing/billable.rb → app/models/pay/paddle_billing/customer.rb} +18 -35
  25. data/{lib → app/models}/pay/paddle_billing/payment_method.rb +2 -12
  26. data/{lib → app/models}/pay/paddle_billing/subscription.rb +9 -33
  27. data/{lib → app/models}/pay/paddle_classic/charge.rb +13 -18
  28. data/{lib/pay/paddle_classic/billable.rb → app/models/pay/paddle_classic/customer.rb} +9 -31
  29. data/{lib → app/models}/pay/paddle_classic/payment_method.rb +1 -11
  30. data/{lib → app/models}/pay/paddle_classic/subscription.rb +11 -36
  31. data/app/models/pay/payment_method.rb +0 -5
  32. data/{lib → app/models}/pay/stripe/charge.rb +6 -22
  33. data/{lib/pay/stripe/billable.rb → app/models/pay/stripe/customer.rb} +73 -108
  34. data/{lib → app/models}/pay/stripe/merchant.rb +2 -11
  35. data/{lib → app/models}/pay/stripe/payment_method.rb +2 -10
  36. data/{lib → app/models}/pay/stripe/subscription.rb +37 -71
  37. data/app/models/pay/subscription.rb +7 -37
  38. data/app/models/pay/webhook.rb +2 -0
  39. data/config/routes.rb +1 -0
  40. data/db/migrate/2_add_pay_sti_columns.rb +24 -0
  41. data/lib/pay/attributes.rb +11 -3
  42. data/lib/pay/braintree.rb +25 -6
  43. data/lib/pay/engine.rb +2 -0
  44. data/lib/pay/fake_processor.rb +2 -6
  45. data/lib/pay/lemon_squeezy/webhooks/order.rb +11 -0
  46. data/lib/pay/lemon_squeezy/webhooks/subscription.rb +3 -3
  47. data/lib/pay/lemon_squeezy/webhooks/subscription_payment.rb +11 -0
  48. data/lib/pay/lemon_squeezy.rb +56 -104
  49. data/lib/pay/paddle_billing.rb +15 -6
  50. data/lib/pay/paddle_classic.rb +11 -9
  51. data/lib/pay/receipts.rb +6 -6
  52. data/lib/pay/stripe/webhooks/checkout_session_completed.rb +1 -1
  53. data/lib/pay/stripe/webhooks/customer_updated.rb +1 -1
  54. data/lib/pay/stripe/webhooks/subscription_trial_will_end.rb +1 -1
  55. data/lib/pay/stripe.rb +21 -7
  56. data/lib/pay/version.rb +1 -1
  57. data/lib/pay.rb +12 -1
  58. metadata +34 -38
  59. data/app/views/pay/stripe/_checkout_button.html.erb +0 -21
  60. data/lib/pay/braintree/authorization_error.rb +0 -9
  61. data/lib/pay/braintree/error.rb +0 -23
  62. data/lib/pay/fake_processor/charge.rb +0 -21
  63. data/lib/pay/fake_processor/error.rb +0 -6
  64. data/lib/pay/fake_processor/payment_method.rb +0 -21
  65. data/lib/pay/fake_processor/subscription.rb +0 -90
  66. data/lib/pay/lemon_squeezy/billable.rb +0 -90
  67. data/lib/pay/lemon_squeezy/charge.rb +0 -68
  68. data/lib/pay/lemon_squeezy/error.rb +0 -7
  69. data/lib/pay/lemon_squeezy/payment_method.rb +0 -40
  70. data/lib/pay/lemon_squeezy/subscription.rb +0 -185
  71. data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +0 -11
  72. data/lib/pay/paddle_billing/error.rb +0 -7
  73. data/lib/pay/paddle_classic/error.rb +0 -7
  74. 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
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
- 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
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,
@@ -79,16 +70,12 @@ module Pay
79
70
  }
80
71
  )
81
72
 
82
- pay_customer.reload_default_payment_method if default
83
- pay_payment_method
84
- end
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
- def trial_end_date(subscription)
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
- 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