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
@@ -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 = find_by(customer: pay_customer, processor_id: object.id))
37
+ pay_subscription.with_lock { pay_subscription.update!(attributes) }
38
+ pay_subscription
39
+ else
40
+ create!(attributes.merge(customer: pay_customer, name: name, processor_id: object.id))
41
+ end
42
+ end
43
+
44
+ def api_record(**options)
45
+ @api_record ||= ::LemonSqueezy::Subscription.retrieve(id: processor_id)
46
+ rescue ::LemonSqueezy::Error => e
47
+ raise Pay::LemonSqueezy::Error, e
48
+ end
49
+
50
+ def portal_url
51
+ api_record.urls.customer_portal
52
+ end
53
+
54
+ def update_url
55
+ api_record.urls.update_payment_method
56
+ end
57
+
58
+ def cancel(**options)
59
+ return if canceled?
60
+ response = ::LemonSqueezy::Subscription.cancel(id: processor_id)
61
+ update(status: response.status, ends_at: response.ends_at)
62
+ rescue ::LemonSqueezy::Error => e
63
+ raise Pay::LemonSqueezy::Error, e
64
+ end
65
+
66
+ def cancel_now!(**options)
67
+ raise Pay::Error, "Lemon Squeezy does not support cancelling immediately through the API."
68
+ end
69
+
70
+ def change_quantity(quantity, **options)
71
+ subscription_item = api_record.first_subscription_item
72
+ ::LemonSqueezy::SubscriptionItem.update(id: subscription_item.id, quantity: quantity)
73
+ update(quantity: quantity)
74
+ rescue ::LemonSqueezy::Error => e
75
+ raise Pay::LemonSqueezy::Error, e
76
+ end
77
+
78
+ # A subscription could be set to cancel or pause in the future
79
+ # It is considered on grace period until the cancel or pause time begins
80
+ def on_grace_period?
81
+ (canceled? && Time.current < ends_at) || (paused? && pause_starts_at? && Time.current < pause_starts_at)
82
+ end
83
+
84
+ def paused?
85
+ status == "paused"
86
+ end
87
+
88
+ def pause(**options)
89
+ response = ::LemonSqueezy::Subscription.pause(id: processor_id, **options)
90
+ update!(status: :paused, pause_starts_at: response.pause&.resumes_at)
91
+ rescue ::LemonSqueezy::Error => e
92
+ raise Pay::LemonSqueezy::Error, e
93
+ end
94
+
95
+ def resumable?
96
+ paused? || canceled?
97
+ end
98
+
99
+ def resume
100
+ unless resumable?
101
+ raise Error, "You can only resume paused or cancelled subscriptions"
102
+ end
103
+
104
+ if paused? && pause_starts_at? && Time.current < pause_starts_at
105
+ ::LemonSqueezy::Subscription.unpause(id: processor_id)
106
+ else
107
+ ::LemonSqueezy::Subscription.uncancel(id: processor_id)
108
+ end
109
+
110
+ update(ends_at: nil, status: :active, pause_starts_at: nil)
111
+ rescue ::LemonSqueezy::Error => e
112
+ raise Pay::LemonSqueezy::Error, e
113
+ end
114
+
115
+ # Lemon Squeezy requires both the Product ID and Variant ID.
116
+ # The Variant ID will be saved as the processor_plan
117
+ def swap(plan, **options)
118
+ raise Error, "A plan_id is required to swap a subscription" unless plan
119
+ raise Error, "A variant_id is required to swap a subscription" unless options[:variant_id]
120
+
121
+ ::LemonSqueezy::Subscription.change_plan id: processor_id, plan_id: plan, variant_id: options[:variant_id]
122
+
123
+ update(processor_plan: options[:variant_id], ends_at: nil, status: :active)
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ ActiveSupport.run_load_hooks :pay_lemon_squeezy_subscription, Pay::LemonSqueezy::Subscription
@@ -6,19 +6,10 @@ 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
23
12
  end
24
13
  end
14
+
15
+ ActiveSupport.run_load_hooks :pay_merchant, Pay::Merchant
@@ -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
@@ -54,15 +48,23 @@ module Pay
54
48
  end
55
49
 
56
50
  # Update or create the charge
57
- if (pay_charge = pay_customer.charges.find_by(processor_id: object.id))
58
- pay_charge.with_lock do
59
- pay_charge.update!(attrs)
60
- end
51
+ if (pay_charge = find_by(customer: pay_customer, processor_id: object.id))
52
+ pay_charge.with_lock { pay_charge.update!(attrs) }
61
53
  pay_charge
62
54
  else
63
- pay_customer.charges.create!(attrs.merge(processor_id: object.id))
55
+ create!(attrs.merge(customer: pay_customer, processor_id: object.id))
56
+ end
57
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
58
+ try += 1
59
+ if try <= retries
60
+ sleep 0.1
61
+ retry
62
+ else
63
+ raise
64
64
  end
65
65
  end
66
66
  end
67
67
  end
68
68
  end
69
+
70
+ ActiveSupport.run_load_hooks :pay_paddle_billing_charge, Pay::PaddleBilling::Charge
@@ -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: pc.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,19 +66,10 @@ 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
90
73
  end
74
+
75
+ ActiveSupport.run_load_hooks :pay_paddle_billing_customer, Pay::PaddleBilling::Customer
@@ -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"
@@ -12,10 +8,10 @@ module Pay
12
8
  sync(pay_customer: pay_customer, attributes: transaction.payments.first)
13
9
  end
14
10
 
15
- def self.sync(pay_customer:, attributes:)
11
+ def self.sync(pay_customer:, attributes:, try: 0, retries: 1)
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
@@ -29,19 +25,23 @@ module Pay
29
25
  payment_method = pay_customer.payment_methods.find_or_initialize_by(processor_id: attributes.payment_method_id)
30
26
  payment_method.update!(attrs)
31
27
  payment_method
28
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
29
+ try += 1
30
+ if try <= retries
31
+ sleep 0.1
32
+ retry
33
+ else
34
+ raise
35
+ end
32
36
  end
33
37
 
34
- def initialize(pay_payment_method)
35
- @pay_payment_method = pay_payment_method
36
- end
37
-
38
- # Sets payment method as default
39
38
  def make_default!
40
39
  end
41
40
 
42
- # Remove payment method
43
41
  def detach
44
42
  end
45
43
  end
46
44
  end
47
45
  end
46
+
47
+ ActiveSupport.run_load_hooks :pay_paddle_billing_payment_method, Pay::PaddleBilling::PaymentMethod
@@ -1,33 +1,15 @@
1
1
  module Pay
2
2
  module PaddleBilling
3
- class Subscription
4
- attr_reader :pay_subscription
5
-
6
- delegate :active?,
7
- :canceled?,
8
- :on_grace_period?,
9
- :on_trial?,
10
- :ends_at,
11
- :name,
12
- :owner,
13
- :pause_starts_at,
14
- :pause_starts_at?,
15
- :processor_id,
16
- :processor_plan,
17
- :processor_subscription,
18
- :prorate,
19
- :prorate?,
20
- :quantity,
21
- :quantity?,
22
- :trial_ends_at,
23
- to: :pay_subscription
3
+ class Subscription < Pay::Subscription
4
+ store_accessor :data, :paddle_update_url
5
+ store_accessor :data, :paddle_cancel_url
24
6
 
25
7
  def self.sync_from_transaction(transaction_id)
26
8
  transaction = ::Paddle::Transaction.retrieve(id: transaction_id)
27
9
  sync(transaction.subscription_id) if transaction.subscription_id
28
10
  end
29
11
 
30
- def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
12
+ def self.sync(subscription_id, object: nil, name: Pay.default_product_name, try: 0, retries: 1)
31
13
  # Passthrough is not return from this API, so we can't use that
32
14
  object ||= ::Paddle::Subscription.retrieve(id: subscription_id)
33
15
 
@@ -75,22 +57,24 @@ module Pay
75
57
  end
76
58
 
77
59
  # Update or create the subscription
78
- if (pay_subscription = pay_customer.subscriptions.find_by(processor_id: subscription_id))
79
- pay_subscription.with_lock do
80
- pay_subscription.update!(attributes)
81
- end
60
+ if (pay_subscription = find_by(customer: pay_customer, processor_id: subscription_id))
61
+ pay_subscription.with_lock { pay_subscription.update!(attributes) }
82
62
  pay_subscription
83
63
  else
84
- pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: subscription_id))
64
+ create!(attributes.merge(customer: pay_customer, name: name, processor_id: subscription_id))
65
+ end
66
+ rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
67
+ try += 1
68
+ if try <= retries
69
+ sleep 0.1
70
+ retry
71
+ else
72
+ raise
85
73
  end
86
74
  end
87
75
 
88
- def initialize(pay_subscription)
89
- @pay_subscription = pay_subscription
90
- end
91
-
92
- def subscription(**options)
93
- @paddle_billing_subscription ||= ::Paddle::Subscription.retrieve(id: processor_id, **options)
76
+ def api_record(**options)
77
+ @api_record ||= ::Paddle::Subscription.retrieve(id: processor_id, **options)
94
78
  end
95
79
 
96
80
  # Get a transaction to update payment method
@@ -105,9 +89,9 @@ module Pay
105
89
 
106
90
  response = ::Paddle::Subscription.cancel(
107
91
  id: processor_id,
108
- effective_from: options.fetch(:effective_from, (paused? ? "immediately" : "next_billing_period"))
92
+ effective_from: options.fetch(:effective_from, paused? ? "immediately" : "next_billing_period")
109
93
  )
110
- pay_subscription.update(
94
+ update(
111
95
  status: response.status,
112
96
  ends_at: response.scheduled_change&.effective_at || Time.current
113
97
  )
@@ -116,7 +100,7 @@ module Pay
116
100
  end
117
101
 
118
102
  def cancel_now!(**options)
119
- cancel(options.merge(effective_from: "immediately"))
103
+ cancel(**options.merge(effective_from: "immediately"))
120
104
  rescue ::Paddle::Error => e
121
105
  raise Pay::PaddleBilling::Error, e
122
106
  end
@@ -127,7 +111,12 @@ module Pay
127
111
  quantity: quantity
128
112
  }]
129
113
 
130
- ::Paddle::Subscription.update(id: processor_id, items: items, proration_billing_mode: "prorated_immediately")
114
+ ::Paddle::Subscription.update(
115
+ id: processor_id,
116
+ items: items,
117
+ proration_billing_mode: options.delete(:proration_billing_mode) || "prorated_immediately"
118
+ )
119
+ update(quantity: quantity)
131
120
  rescue ::Paddle::Error => e
132
121
  raise Pay::PaddleBilling::Error, e
133
122
  end
@@ -139,12 +128,12 @@ module Pay
139
128
  end
140
129
 
141
130
  def paused?
142
- pay_subscription.status == "paused"
131
+ status == "paused"
143
132
  end
144
133
 
145
134
  def pause
146
135
  response = ::Paddle::Subscription.pause(id: processor_id)
147
- pay_subscription.update!(status: :paused, pause_starts_at: response.scheduled_change.effective_at)
136
+ update!(status: :paused, pause_starts_at: response.scheduled_change.effective_at)
148
137
  rescue ::Paddle::Error => e
149
138
  raise Pay::PaddleBilling::Error, e
150
139
  end
@@ -155,7 +144,7 @@ module Pay
155
144
 
156
145
  def resume
157
146
  unless resumable?
158
- raise StandardError, "You can only resume paused subscriptions."
147
+ raise Error, "You can only resume paused subscriptions."
159
148
  end
160
149
 
161
150
  # Paddle Billing API only allows "resuming" subscriptions when they are paused
@@ -166,19 +155,25 @@ module Pay
166
155
  ::Paddle::Subscription.resume(id: processor_id, effective_from: "immediately")
167
156
  end
168
157
 
169
- pay_subscription.update(status: :active, pause_starts_at: nil)
158
+ update(ends_at: nil, status: :active, pause_starts_at: nil)
170
159
  rescue ::Paddle::Error => e
171
160
  raise Pay::PaddleBilling::Error, e
172
161
  end
173
162
 
174
163
  def swap(plan, **options)
164
+ raise ArgumentError, "plan must be a string" unless plan.is_a?(String)
165
+
175
166
  items = [{
176
167
  price_id: plan,
177
168
  quantity: quantity || 1
178
169
  }]
179
170
 
180
- ::Paddle::Subscription.update(id: processor_id, items: items, proration_billing_mode: "prorated_immediately")
181
- pay_subscription.update(processor_plan: plan, ends_at: nil, status: :active)
171
+ ::Paddle::Subscription.update(
172
+ id: processor_id,
173
+ items: items,
174
+ proration_billing_mode: options.delete(:proration_billing_mode) || "prorated_immediately"
175
+ )
176
+ update(processor_plan: plan, ends_at: nil, status: :active)
182
177
  end
183
178
 
184
179
  # Retries the latest invoice for a Past Due subscription
@@ -187,3 +182,5 @@ module Pay
187
182
  end
188
183
  end
189
184
  end
185
+
186
+ ActiveSupport.run_load_hooks :pay_paddle_billing_subscription, Pay::PaddleBilling::Subscription
@@ -1,35 +1,32 @@
1
1
  module Pay
2
2
  module PaddleClassic
3
- class Charge
4
- attr_reader :pay_charge
3
+ class Charge < Pay::Charge
4
+ store_accessor :data, :paddle_receipt_url
5
5
 
6
- delegate :processor_id, :customer, to: :pay_charge
7
-
8
- def initialize(pay_charge)
9
- @pay_charge = pay_charge
10
- end
11
-
12
- def charge
6
+ def api_record
13
7
  return unless customer.subscription
8
+
14
9
  payments = PaddleClassic.client.payments.list(subscription_id: customer.subscription.processor_id)
15
10
  charges = payments.data.select { |p| p[:id].to_s == processor_id }
16
11
  charges.try(:first)
17
- rescue ::Paddle::Error => e
12
+ rescue ::Paddle::Classic::Error => e
18
13
  raise Pay::PaddleClassic::Error, e
19
14
  end
20
15
 
21
- def refund!(amount_to_refund)
16
+ def refund!(amount_to_refund = nil)
22
17
  return unless customer.subscription
18
+ amount_to_refund ||= amount
19
+
23
20
  payments = PaddleClassic.client.payments.list(subscription_id: customer.subscription.processor_id, is_paid: 1)
24
- if payments.total > 0
25
- PaddleClassic.client.payments.refund(order_id: payments.data.last[:id], amount: amount_to_refund)
26
- pay_charge.update(amount_refunded: amount_to_refund)
27
- else
28
- raise Error, "Payment not found"
29
- end
30
- rescue ::Paddle::Error => e
21
+ raise Error, "Payment not found" unless payments.total > 0
22
+
23
+ PaddleClassic.client.payments.refund(order_id: payments.data.last[:id], amount: amount_to_refund)
24
+ update(amount_refunded: amount_to_refund)
25
+ rescue ::Paddle::Classic::Error => e
31
26
  raise Pay::PaddleClassic::Error, e
32
27
  end
33
28
  end
34
29
  end
35
30
  end
31
+
32
+ ActiveSupport.run_load_hooks :pay_paddle_classic_charge, Pay::PaddleClassic::Charge
@@ -1,29 +1,18 @@
1
1
  module Pay
2
2
  module PaddleClassic
3
- class Billable
4
- attr_reader :pay_customer
3
+ class Customer < Pay::Customer
4
+ has_many :charges, dependent: :destroy, class_name: "Pay::PaddleClassic::Charge"
5
+ has_many :subscriptions, dependent: :destroy, class_name: "Pay::PaddleClassic::Subscription"
6
+ has_many :payment_methods, dependent: :destroy, class_name: "Pay::PaddleClassic::PaymentMethod"
7
+ has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::PaddleClassic::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
- # pass
9
+ def api_record
19
10
  end
20
11
 
21
- def update_customer!
22
- # pass
12
+ def update_api_record
23
13
  end
24
14
 
25
15
  def charge(amount, options = {})
26
- subscription = pay_customer.subscription
27
16
  return unless subscription.processor_id
28
17
  raise Pay::Error, "A charge_name is required to create a one-time charge" if options[:charge_name].nil?
29
18
 
@@ -38,7 +27,7 @@ module Pay
38
27
  # Lookup subscription payment method details
39
28
  attributes.merge! Pay::PaddleClassic::PaymentMethod.payment_method_details_for(subscription_id: subscription.processor_id)
40
29
 
41
- charge = pay_customer.charges.find_or_initialize_by(processor_id: response[:invoice_id])
30
+ charge = charges.find_or_initialize_by(processor_id: response[:invoice_id])
42
31
  charge.update(attributes)
43
32
  charge
44
33
  rescue ::Paddle::Error => e
@@ -52,19 +41,10 @@ module Pay
52
41
  # Paddle does not use payment method tokens. The method signature has it here
53
42
  # to have a uniform API with the other payment processors.
54
43
  def add_payment_method(token = nil, default: true)
55
- Pay::PaddleClassic::PaymentMethod.sync(pay_customer: pay_customer)
56
- end
57
-
58
- def trial_end_date(subscription)
59
- return unless subscription.state == "trialing"
60
- Time.zone.parse(subscription.next_payment[:date]).end_of_day
61
- end
62
-
63
- def processor_subscription(subscription_id, options = {})
64
- PaddleClassic.client.users.list(subscription_id: subscription_id).data.try(:first)
65
- rescue ::Paddle::Error => e
66
- raise Pay::PaddleClassic::Error, e
44
+ Pay::PaddleClassic::PaymentMethod.sync(pay_customer: self)
67
45
  end
68
46
  end
69
47
  end
70
48
  end
49
+
50
+ ActiveSupport.run_load_hooks :pay_paddle_classic_customer, Pay::PaddleClassic::Customer
@@ -1,10 +1,6 @@
1
1
  module Pay
2
2
  module PaddleClassic
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
  # Paddle doesn't provide PaymentMethod IDs, so we have to lookup via the Customer
9
5
  def self.sync(pay_customer:, attributes: nil)
10
6
  return unless pay_customer.subscription
@@ -44,17 +40,13 @@ module Pay
44
40
  end
45
41
  end
46
42
 
47
- def initialize(pay_payment_method)
48
- @pay_payment_method = pay_payment_method
49
- end
50
-
51
- # Sets payment method as default
52
43
  def make_default!
53
44
  end
54
45
 
55
- # Remove payment method
56
46
  def detach
57
47
  end
58
48
  end
59
49
  end
60
50
  end
51
+
52
+ ActiveSupport.run_load_hooks :pay_paddle_classic_payment_method, Pay::PaddleClassic::PaymentMethod