pay 7.3.0 → 8.1.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 (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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ec013c6c05d0e8027b628258882c9bac3a01f7942e9654cc52582643cf57629
4
- data.tar.gz: 75bcbbfcd8b63bb0f73e97b4a4da41335923f5f3af2e8577fc1f2aa94baa8542
3
+ metadata.gz: 5fe510d1e6a11611a8cd4678e965e1051f6e69ad6c404f55c3fa96312dfff51a
4
+ data.tar.gz: 54b73170c35476916fd17dc4ca11a7bc5d117888a04385e0947ef26dfd069e8f
5
5
  SHA512:
6
- metadata.gz: dd2822689a009a126f016365182312e4555098ba40b429e16c1dc41b76902a4fd18da06e3465b25cf17e0bf53371adf147bfd3c72e2c9186ce965de304f403e4
7
- data.tar.gz: 1ad32b2e21be33e58ffb855dec4275cdff66d036a1728f2573d54d61a2937c6aa6c4602cb59f1dc53bea2edcd4fac46f89efcb91a5ea22443669087569959e5b
6
+ metadata.gz: b55b7a23acd69d6f1a795805ba707f3cdbbceca636359ea509419a7a547764e048ce3c36c82f58b0fd34a97e2dac0561b76266ae42b7d62f39d6d9ce38f25944
7
+ data.tar.gz: ed7f92af40c8bfe066668a8c20212df7c316eb237db7091a5d44ca60e728c037e12d5e846ec6e4b87b3087cf22733568f101ceda2132ff945367b9245a46975a
data/README.md CHANGED
@@ -23,6 +23,7 @@ Our supported payment processors are:
23
23
  - Stripe ([SCA Compatible](https://stripe.com/docs/strong-customer-authentication) using API version `2022-11-15`)
24
24
  - Paddle (SCA Compatible & supports PayPal)
25
25
  - Braintree (supports PayPal)
26
+ - Lemon Squeezy (supports PayPal)
26
27
  - [Fake Processor](docs/fake_processor/1_overview.md) (used for generic trials without cards, free subscriptions, testing, etc)
27
28
 
28
29
  Want to add a new payment provider? Contributions are welcome.
@@ -45,6 +46,7 @@ Want to add a new payment provider? Contributions are welcome.
45
46
  * [Stripe](docs/stripe/1_overview.md)
46
47
  * [Braintree](docs/braintree/1_overview.md)
47
48
  * [Paddle](docs/paddle_billing/1_overview.md)
49
+ * [Lemon Squeezy](docs/lemon_squeezy/1_overview.md)
48
50
  * [Fake Processor](docs/fake_processor/1_overview.md)
49
51
  * **Marketplaces**
50
52
  * [Stripe Connect](docs/marketplaces/stripe_connect.md)
@@ -0,0 +1,45 @@
1
+ module Pay
2
+ module Webhooks
3
+ class LemonSqueezyController < Pay::ApplicationController
4
+ if Rails.application.config.action_controller.default_protect_from_forgery
5
+ skip_before_action :verify_authenticity_token
6
+ end
7
+
8
+ def create
9
+ if valid_signature?(request.headers["X-Signature"])
10
+ queue_event(verify_params.as_json)
11
+ head :ok
12
+ else
13
+ head :bad_request
14
+ end
15
+ rescue Pay::LemonSqueezy::Error
16
+ head :bad_request
17
+ end
18
+
19
+ private
20
+
21
+ def queue_event(event)
22
+ return unless Pay::Webhooks.delegator.listening?("lemon_squeezy.#{params[:meta][:event_name]}")
23
+
24
+ record = Pay::Webhook.create!(processor: :lemon_squeezy, event_type: params[:meta][:event_name], event: event)
25
+ Pay::Webhooks::ProcessJob.perform_later(record)
26
+ end
27
+
28
+ # Pass Lemon Squeezy signature from request.headers["X-Signature"]
29
+ def valid_signature?(signature)
30
+ return false if signature.blank?
31
+
32
+ key = Pay::LemonSqueezy.signing_secret
33
+ data = request.raw_post
34
+ digest = OpenSSL::Digest.new("sha256")
35
+
36
+ hmac = OpenSSL::HMAC.hexdigest(digest, key, data)
37
+ ActiveSupport::SecurityUtils.secure_compare(hmac, signature)
38
+ end
39
+
40
+ def verify_params
41
+ params.except(:action, :controller).permit!
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,7 +1,7 @@
1
1
  module Pay
2
2
  class CustomerSyncJob < ApplicationJob
3
3
  def perform(pay_customer_id)
4
- Pay::Customer.find(pay_customer_id).update_customer!
4
+ Pay::Customer.find(pay_customer_id).update_api_record
5
5
  rescue ActiveRecord::RecordNotFound
6
6
  Rails.logger.info "Couldn't find a Pay::Customer with ID = #{pay_customer_id}"
7
7
  end
@@ -0,0 +1,13 @@
1
+ module Pay
2
+ module Routing
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include Rails.application.routes.url_helpers
7
+ end
8
+
9
+ def default_url_options
10
+ Rails.application.config.action_mailer.default_url_options || {}
11
+ end
12
+ end
13
+ end
@@ -1,10 +1,6 @@
1
1
  module Pay
2
2
  module Braintree
3
- class Charge
4
- attr_reader :pay_charge
5
-
6
- delegate :processor_id, to: :pay_charge
7
-
3
+ class Charge < Pay::Charge
8
4
  def self.sync(charge_id, object: nil, try: 0, retries: 1)
9
5
  object ||= Pay.braintree_gateway.transaction.find(charge_id)
10
6
 
@@ -22,19 +18,16 @@ module Pay
22
18
  end
23
19
  end
24
20
 
25
- def initialize(pay_charge)
26
- @pay_charge = pay_charge
27
- end
28
-
29
- def charge
21
+ def api_record
30
22
  Pay.braintree_gateway.transaction.find(processor_id)
31
23
  rescue ::Braintree::Braintree::Error => e
32
24
  raise Pay::Braintree::Error, e
33
25
  end
34
26
 
35
- def refund!(amount_to_refund)
27
+ def refund!(amount_to_refund = nil)
28
+ amount_to_refund ||= amount
36
29
  Pay.braintree_gateway.transaction.refund(processor_id, amount_to_refund / 100.0)
37
- pay_charge.update(amount_refunded: amount_to_refund)
30
+ update(amount_refunded: amount_to_refund)
38
31
  rescue ::Braintree::BraintreeError => e
39
32
  raise Pay::Braintree::Error, e
40
33
  end
@@ -1,62 +1,34 @@
1
1
  module Pay
2
2
  module Braintree
3
- class Billable
4
- attr_reader :pay_customer
5
-
6
- delegate :processor_id,
7
- :processor_id?,
8
- :email,
9
- :customer_name,
10
- :payment_method_token,
11
- :payment_method_token?,
12
- to: :pay_customer
13
-
14
- def initialize(pay_customer)
15
- @pay_customer = pay_customer
16
- end
3
+ class Customer < Pay::Customer
4
+ has_many :charges, dependent: :destroy, class_name: "Pay::Braintree::Charge"
5
+ has_many :subscriptions, dependent: :destroy, class_name: "Pay::Braintree::Subscription"
6
+ has_many :payment_methods, dependent: :destroy, class_name: "Pay::Braintree::PaymentMethod"
7
+ has_one :default_payment_method, -> { where(default: true) }, class_name: "Pay::Braintree::PaymentMethod"
17
8
 
18
9
  # Returns a hash of attributes for the Stripe::Customer object
19
- def customer_attributes
20
- owner = pay_customer.owner
21
-
10
+ def api_record_attributes
22
11
  attributes = case owner.class.pay_braintree_customer_attributes
23
12
  when Symbol
24
- owner.send(owner.class.pay_braintree_customer_attributes, pay_customer)
13
+ owner.send(owner.class.pay_braintree_customer_attributes, self)
25
14
  when Proc
26
- owner.class.pay_braintree_customer_attributes.call(pay_customer)
15
+ owner.class.pay_braintree_customer_attributes.call(self)
27
16
  end
28
17
 
29
- # Guard against attributes being returned nil
30
- attributes ||= {}
31
-
32
18
  first_name, last_name = customer_name.split(" ", 2)
33
- {email: email, first_name: first_name, last_name: last_name}.merge(attributes)
19
+ {email: email, first_name: first_name, last_name: last_name}.merge(attributes || {})
34
20
  end
35
21
 
36
22
  # Retrieve the Braintree::Customer object
37
23
  #
38
24
  # - If no processor_id is present, creates a Customer.
39
- # - When 'payment_method_token' is present, it will also set the default payment method
40
- def customer
25
+ def api_record
41
26
  if processor_id?
42
- customer = gateway.customer.find(processor_id)
43
-
44
- if payment_method_token?
45
- add_payment_method(payment_method_token, default: true)
46
- pay_customer.payment_method_token = nil
47
- end
48
-
49
- customer
27
+ gateway.customer.find(processor_id)
50
28
  else
51
- result = gateway.customer.create(customer_attributes.merge(payment_method_nonce: payment_method_token))
29
+ result = gateway.customer.create(api_record_attributes)
52
30
  raise Pay::Braintree::Error, result unless result.success?
53
- pay_customer.update!(processor_id: result.customer.id)
54
-
55
- if payment_method_token?
56
- save_payment_method(result.customer.payment_methods.last, default: true)
57
- pay_customer.payment_method_token = nil
58
- end
59
-
31
+ update!(processor_id: result.customer.id)
60
32
  result.customer
61
33
  end
62
34
  rescue ::Braintree::AuthorizationError => e
@@ -67,15 +39,15 @@ module Pay
67
39
 
68
40
  # Syncs name and email to Braintree::Customer
69
41
  # You can also pass in other attributes that will be merged into the default attributes
70
- def update_customer!(**attributes)
71
- customer unless processor_id?
72
- gateway.customer.update(processor_id, customer_attributes.merge(attributes))
42
+ def update_api_record(**attributes)
43
+ api_record unless processor_id?
44
+ gateway.customer.update(processor_id, api_record_attributes.merge(attributes))
73
45
  end
74
46
 
75
47
  def charge(amount, options = {})
76
48
  args = {
77
49
  amount: amount.to_i / 100.0,
78
- customer_id: customer.id,
50
+ customer_id: processor_id || api_record.id,
79
51
  options: {submit_for_settlement: true},
80
52
  custom_fields: options.delete(:metadata)
81
53
  }.merge(options)
@@ -91,7 +63,7 @@ module Pay
91
63
  end
92
64
 
93
65
  def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
94
- token = customer.payment_methods.find(&:default?).try(:token)
66
+ token = api_record.payment_methods.find(&:default?).try(:token)
95
67
  raise Pay::Error, "Customer has no default payment method" if token.nil?
96
68
 
97
69
  # Standardize the trial period options
@@ -100,20 +72,20 @@ module Pay
100
72
  end
101
73
 
102
74
  metadata = options.delete(:metadata)
103
- subscription_options = options.merge(
104
- payment_method_token: token,
105
- plan_id: plan
106
- )
75
+ subscription_options = options.merge(payment_method_token: token, plan_id: plan)
107
76
 
108
77
  result = gateway.subscription.create(subscription_options)
109
78
  raise Pay::Braintree::Error, result unless result.success?
110
79
 
111
- subscription = pay_customer.subscriptions.create!(
80
+ # Braintree returns dates without time zones, so we'll assume they're UTC
81
+ trial_end_date = result.subscription.trial_period.present? ? result.subscription.first_billing_date.end_of_day : nil
82
+
83
+ subscription = subscriptions.create!(
112
84
  name: name,
113
85
  processor_id: result.subscription.id,
114
86
  processor_plan: plan,
115
87
  status: :active,
116
- trial_ends_at: trial_end_date(result.subscription),
88
+ trial_ends_at: trial_end_date,
117
89
  ends_at: nil,
118
90
  metadata: metadata
119
91
  )
@@ -130,10 +102,8 @@ module Pay
130
102
  end
131
103
 
132
104
  def add_payment_method(token, default: false)
133
- customer unless processor_id?
134
-
135
105
  result = gateway.payment_method.create(
136
- customer_id: processor_id,
106
+ customer_id: processor_id || api_record.id,
137
107
  payment_method_nonce: token,
138
108
  options: {
139
109
  make_default: default,
@@ -145,7 +115,7 @@ module Pay
145
115
  pay_payment_method = save_payment_method(result.payment_method, default: default)
146
116
 
147
117
  # Update existing subscriptions to the new payment method
148
- pay_customer.subscriptions.each do |subscription|
118
+ subscriptions.each do |subscription|
149
119
  if subscription.active?
150
120
  gateway.subscription.update(subscription.processor_id, {payment_method_token: token})
151
121
  end
@@ -158,16 +128,6 @@ module Pay
158
128
  raise Pay::Braintree::Error, e
159
129
  end
160
130
 
161
- def trial_end_date(subscription)
162
- return unless subscription.trial_period
163
- # Braintree returns dates without time zones, so we'll assume they're UTC
164
- subscription.first_billing_date.end_of_day
165
- end
166
-
167
- def processor_subscription(subscription_id, options = {})
168
- gateway.subscription.find(subscription_id)
169
- end
170
-
171
131
  def save_transaction(transaction)
172
132
  attrs = card_details_for_braintree_transaction(transaction)
173
133
  attrs[:amount] = transaction.amount.to_f * 100
@@ -178,7 +138,7 @@ module Pay
178
138
 
179
139
  # Associate charge with subscription if we can
180
140
  if transaction.subscription_id
181
- pay_subscription = pay_customer.subscriptions.find_by(processor_id: transaction.subscription_id)
141
+ pay_subscription = subscriptions.find_by(processor_id: transaction.subscription_id)
182
142
  pay_subscription ||= Pay::Braintree::Subscription.sync(transaction.subscription_id)
183
143
 
184
144
  if pay_subscription
@@ -187,7 +147,7 @@ module Pay
187
147
  end
188
148
  end
189
149
 
190
- charge = pay_customer.charges.find_or_initialize_by(processor_id: transaction.id)
150
+ charge = charges.find_or_initialize_by(processor_id: transaction.id)
191
151
  charge.update!(attrs)
192
152
  charge
193
153
  end
@@ -238,13 +198,13 @@ module Pay
238
198
  }
239
199
  end
240
200
 
241
- pay_payment_method = pay_customer.payment_methods.where(processor_id: payment_method.token).first_or_initialize
201
+ pay_payment_method = payment_methods.where(processor_id: payment_method.token).first_or_initialize
242
202
 
243
- pay_customer.payment_methods.update_all(default: false) if default
203
+ payment_methods.update_all(default: false) if default
244
204
  pay_payment_method.update!(attributes.merge(default: default))
245
205
 
246
206
  # Reload the Rails association
247
- pay_customer.reload_default_payment_method if default
207
+ reload_default_payment_method if default
248
208
 
249
209
  pay_payment_method
250
210
  end
@@ -1,10 +1,6 @@
1
1
  module Pay
2
2
  module Braintree
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(id, object: nil, try: 0, retries: 1)
9
5
  object ||= Pay.braintree_gateway.payment_method.find(id)
10
6
 
@@ -14,10 +10,6 @@ module Pay
14
10
  pay_customer.save_payment_method(object, default: object.default?)
15
11
  end
16
12
 
17
- def initialize(pay_payment_method)
18
- @pay_payment_method = pay_payment_method
19
- end
20
-
21
13
  # Sets payment method as default on Stripe
22
14
  def make_default!
23
15
  result = gateway.customer.update(customer.processor_id, default_payment_method_token: processor_id)
@@ -1,25 +1,6 @@
1
1
  module Pay
2
2
  module Braintree
3
- class Subscription
4
- attr_reader :pay_subscription
5
-
6
- delegate :active?,
7
- :canceled?,
8
- :ends_at?,
9
- :customer,
10
- :ends_at,
11
- :name,
12
- :on_trial?,
13
- :processor_id,
14
- :processor_plan,
15
- :processor_subscription,
16
- :prorate,
17
- :prorate?,
18
- :quantity,
19
- :quantity?,
20
- :trial_ends_at,
21
- to: :pay_subscription
22
-
3
+ class Subscription < Pay::Subscription
23
4
  def self.sync(subscription_id, object: nil, name: nil, try: 0, retries: 1)
24
5
  object ||= Pay.braintree_gateway.subscription.find(subscription_id)
25
6
 
@@ -69,11 +50,7 @@ module Pay
69
50
  end
70
51
  end
71
52
 
72
- def initialize(pay_subscription)
73
- @pay_subscription = pay_subscription
74
- end
75
-
76
- def subscription(**options)
53
+ def api_record(**options)
77
54
  gateway.subscription.find(processor_id)
78
55
  end
79
56
 
@@ -84,11 +61,9 @@ module Pay
84
61
  result = if on_trial?
85
62
  gateway.subscription.cancel(processor_id)
86
63
  else
87
- gateway.subscription.update(subscription.id, {
88
- number_of_billing_cycles: subscription.current_billing_cycle
89
- })
64
+ gateway.subscription.update(processor_id, {number_of_billing_cycles: api_record.current_billing_cycle})
90
65
  end
91
- pay_subscription.sync!(object: result.subscription)
66
+ sync!(object: result.subscription)
92
67
  rescue ::Braintree::BraintreeError => e
93
68
  raise Pay::Braintree::Error, e
94
69
  end
@@ -97,7 +72,7 @@ module Pay
97
72
  return if canceled?
98
73
 
99
74
  result = gateway.subscription.cancel(processor_id)
100
- pay_subscription.sync!(object: result.subscription)
75
+ sync!(object: result.subscription)
101
76
  rescue ::Braintree::BraintreeError => e
102
77
  raise Pay::Braintree::Error, e
103
78
  end
@@ -106,10 +81,6 @@ module Pay
106
81
  raise NotImplementedError, "Braintree does not support setting quantity on subscriptions"
107
82
  end
108
83
 
109
- def on_grace_period?
110
- ends_at? && ends_at > Time.current
111
- end
112
-
113
84
  def paused?
114
85
  false
115
86
  end
@@ -138,15 +109,13 @@ module Pay
138
109
  trial_duration_unit: :day
139
110
  )
140
111
  else
141
- subscription = processor_subscription
142
-
143
- gateway.subscription.update(subscription.id, {
112
+ gateway.subscription.update(processor_id, {
144
113
  never_expires: true,
145
114
  number_of_billing_cycles: nil
146
115
  })
147
116
  end
148
117
 
149
- pay_subscription.update(status: :active)
118
+ update(ends_at: nil, status: :active)
150
119
  rescue ::Braintree::BraintreeError => e
151
120
  raise Pay::Braintree::Error, e
152
121
  end
@@ -171,9 +140,7 @@ module Pay
171
140
  return
172
141
  end
173
142
 
174
- subscription = processor_subscription
175
-
176
- result = gateway.subscription.update(subscription.id, {
143
+ result = gateway.subscription.update(processor_id, {
177
144
  plan_id: braintree_plan.id,
178
145
  price: braintree_plan.price,
179
146
  never_expires: true,
@@ -184,7 +151,7 @@ module Pay
184
151
  })
185
152
  raise Error, "Braintree failed to swap plans: #{result.message}" unless result.success?
186
153
 
187
- pay_subscription.update(processor_plan: plan, ends_at: nil, status: :active)
154
+ update(processor_plan: plan, ends_at: nil, status: :active)
188
155
  rescue ::Braintree::BraintreeError => e
189
156
  raise Pay::Braintree::Error, e
190
157
  end
@@ -198,7 +165,7 @@ module Pay
198
165
  )
199
166
 
200
167
  if result.success?
201
- pay_subscription.update(status: :active)
168
+ update(status: :active)
202
169
  end
203
170
  end
204
171
 
@@ -224,30 +191,25 @@ module Pay
224
191
 
225
192
  def discount_for_switching_to_monthly(current_plan, plan)
226
193
  cycles = (money_remaining_on_yearly_plan(current_plan) / plan.price).floor
227
- OpenStruct.new(
194
+ ActiveSupport::InheritableOptions.new(
228
195
  amount: plan.price,
229
196
  number_of_billing_cycles: cycles
230
197
  )
231
198
  end
232
199
 
233
200
  def money_remaining_on_yearly_plan(current_plan)
234
- end_date = processor_subscription.billing_period_end_date.to_date
201
+ end_date = api_record.billing_period_end_date.to_date
235
202
  (current_plan.price / 365) * (end_date - Date.today)
236
203
  end
237
204
 
238
- def discount_for_switching_to_yearly
239
- amount = 0
240
-
241
- processor_subscription.discounts.each do |discount|
205
+ def discount_for_switching_to_yearly(amount: 0)
206
+ api_record.discounts.each do |discount|
242
207
  if discount.id == "plan-credit"
243
208
  amount += discount.amount * discount.number_of_billing_cycles
244
209
  end
245
210
  end
246
211
 
247
- OpenStruct.new(
248
- amount: amount,
249
- number_of_billing_cycles: 1
250
- )
212
+ ActiveSupport::InheritableOptions.new(amount: amount, number_of_billing_cycles: 1)
251
213
  end
252
214
 
253
215
  def swap_across_frequencies(plan)
@@ -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,10 +13,6 @@ 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
21
-
22
16
  # Payment method attributes
23
17
  store_accessor :data, :payment_method_type # card, paypal, sepa, etc
24
18
  store_accessor :data, :brand # Visa, Mastercard, Discover, PayPal
@@ -44,7 +38,7 @@ module Pay
44
38
  store_accessor :data, :refunds # array of refunds
45
39
 
46
40
  # Helpers for payment processors
47
- %w[braintree stripe paddle_billing paddle_classic fake_processor].each do |processor_name|
41
+ %w[braintree stripe paddle_billing paddle_classic lemon_squeezy fake_processor].each do |processor_name|
48
42
  define_method :"#{processor_name}?" do
49
43
  customer.processor == processor_name
50
44
  end
@@ -52,33 +46,14 @@ module Pay
52
46
  scope processor_name, -> { joins(:customer).where(pay_customers: {processor: processor_name}) }
53
47
  end
54
48
 
55
- delegate :capture, :credit_note!, :credit_notes, to: :payment_processor
56
-
57
49
  def self.find_by_processor_and_id(processor, processor_id)
58
50
  joins(:customer).find_by(processor_id: processor_id, pay_customers: {processor: processor})
59
51
  end
60
52
 
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
53
  def captured?
74
54
  amount_captured > 0
75
55
  end
76
56
 
77
- def refund!(refund_amount = nil)
78
- refund_amount ||= amount
79
- payment_processor.refund!(refund_amount)
80
- end
81
-
82
57
  def refunded?
83
58
  amount_refunded.to_i > 0
84
59
  end
@@ -104,7 +79,13 @@ module Pay
104
79
  when "card"
105
80
  "#{brand.titleize} (**** **** **** #{last4})"
106
81
  when "paypal"
107
- "#{brand} (#{email})"
82
+ # Sometimes brand and email are missing (Stripe)
83
+ brand ||= "PayPal"
84
+ if email.present?
85
+ brand + " (#{email})"
86
+ else
87
+ brand
88
+ end
108
89
 
109
90
  # Braintree
110
91
  when "venmo"
@@ -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
 
@@ -23,9 +21,8 @@ module Pay
23
21
  store_accessor :data, :currency
24
22
 
25
23
  delegate :email, to: :owner
26
- delegate_missing_to :pay_processor
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)
@@ -0,0 +1,13 @@
1
+ module Pay
2
+ module FakeProcessor
3
+ class Charge < Pay::Charge
4
+ def api_record
5
+ self
6
+ end
7
+
8
+ def refund!(amount_to_refund = nil)
9
+ update(amount_refunded: amount_to_refund || amount)
10
+ end
11
+ end
12
+ end
13
+ end