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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ec013c6c05d0e8027b628258882c9bac3a01f7942e9654cc52582643cf57629
4
- data.tar.gz: 75bcbbfcd8b63bb0f73e97b4a4da41335923f5f3af2e8577fc1f2aa94baa8542
3
+ metadata.gz: 0e194484cea3ae3dee4d9fdded7e5732f182a5e7699128f2f86bfa020284806f
4
+ data.tar.gz: 14b67bbf65d49fb2d7d73fdfbc8519baf5ddcade178ba88bd5b5b30534413d09
5
5
  SHA512:
6
- metadata.gz: dd2822689a009a126f016365182312e4555098ba40b429e16c1dc41b76902a4fd18da06e3465b25cf17e0bf53371adf147bfd3c72e2c9186ce965de304f403e4
7
- data.tar.gz: 1ad32b2e21be33e58ffb855dec4275cdff66d036a1728f2573d54d61a2937c6aa6c4602cb59f1dc53bea2edcd4fac46f89efcb91a5ea22443669087569959e5b
6
+ metadata.gz: b2c00bd4c90a603a91b62202ba36c5cb3cfdd99fcacdd347e274c2bf6c6cb4d813115373e23c9a3e575559d3ec0d37561c8f9a08a60e028415dace786dfcbb16
7
+ data.tar.gz: d62df086e5566fa6c25304c8cdbb54ef9ef401ba0a3023541085c57ab32214287c953d78f1c86be4aeaeb34cc26c29635bc00e4e4b757f7f3579d65fa2ceb02b
data/README.md CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  # 💳 Pay - Payments engine for Ruby on Rails
4
4
 
5
- [![Build Status](https://github.com/pay-rails/pay/workflows/Tests/badge.svg)](https://github.com/pay-rails/pay/actions) [![Gem Version](https://badge.fury.io/rb/pay.svg)](https://badge.fury.io/rb/pay)
6
-
7
- <img src="docs/images/stripe_partner_badge.svg" height="26px">
5
+ [![Build Status](https://github.com/pay-rails/pay/workflows/Tests/badge.svg)](https://github.com/pay-rails/pay/actions) [![Gem Version](https://badge.fury.io/rb/pay.svg)](https://badge.fury.io/rb/pay) <img src="docs/images/stripe_partner_badge.svg" height="26px">
8
6
 
9
7
  Pay is a payments engine for Ruby on Rails 6.0 and higher.
10
8
 
11
- ⚠️ **Upgrading?** Check the [UPGRADE](UPGRADE.md) guide for required changes and/or migration when upgrading from a previous version of Pay.
9
+ > [!TIP]
10
+ > Check out [Jumpstart](https://jumpstartrails.com) for Rails Starter Kit with Pay already integrated!
11
+
12
+ **Upgrading?** Check the [UPGRADE](UPGRADE.md) guide for required changes and/or migration when upgrading from a previous version of Pay.
12
13
 
13
14
  ## 🧑‍💻 Tutorial
14
15
 
@@ -23,6 +24,7 @@ Our supported payment processors are:
23
24
  - Stripe ([SCA Compatible](https://stripe.com/docs/strong-customer-authentication) using API version `2022-11-15`)
24
25
  - Paddle (SCA Compatible & supports PayPal)
25
26
  - Braintree (supports PayPal)
27
+ - Lemon Squeezy (supports PayPal)
26
28
  - [Fake Processor](docs/fake_processor/1_overview.md) (used for generic trials without cards, free subscriptions, testing, etc)
27
29
 
28
30
  Want to add a new payment provider? Contributions are welcome.
@@ -45,7 +47,9 @@ Want to add a new payment provider? Contributions are welcome.
45
47
  * [Stripe](docs/stripe/1_overview.md)
46
48
  * [Braintree](docs/braintree/1_overview.md)
47
49
  * [Paddle](docs/paddle_billing/1_overview.md)
50
+ * [Lemon Squeezy](docs/lemon_squeezy/1_overview.md)
48
51
  * [Fake Processor](docs/fake_processor/1_overview.md)
52
+ * [Asaas (Community)](https://github.com/PedroAugustoRamalhoDuarte/pay-asaas)
49
53
  * **Marketplaces**
50
54
  * [Stripe Connect](docs/marketplaces/stripe_connect.md)
51
55
  * **Contributing**
@@ -6,6 +6,8 @@ module Pay
6
6
 
7
7
  def show
8
8
  @payment = Payment.from_id(params[:id])
9
+ rescue ::Stripe::StripeError => e
10
+ redirect_to root_path, alert: e.message
9
11
  end
10
12
 
11
13
  private
@@ -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,22 +18,21 @@ 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
41
34
  end
42
35
  end
43
36
  end
37
+
38
+ ActiveSupport.run_load_hooks :pay_braintree_charge, Pay::Braintree::Charge
@@ -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 = Pay::Braintree::Charge.find_or_initialize_by(customer: self, 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
@@ -302,3 +262,5 @@ module Pay
302
262
  end
303
263
  end
304
264
  end
265
+
266
+ ActiveSupport.run_load_hooks :pay_braintree_customer, Pay::Braintree::Customer
@@ -1,23 +1,15 @@
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
 
11
- pay_customer = Pay::Customer.find_by(processor: :braintree, processor_id: object.customer_id)
7
+ pay_customer = Pay::Braintree::Customer.find_by(processor_id: object.customer_id)
12
8
  return unless pay_customer
13
9
 
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)
@@ -40,3 +32,5 @@ module Pay
40
32
  end
41
33
  end
42
34
  end
35
+
36
+ ActiveSupport.run_load_hooks :pay_braintree_payment_method, Pay::Braintree::PaymentMethod
@@ -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
 
@@ -50,15 +31,12 @@ module Pay
50
31
  attributes[:ends_at] = object.paid_through_date.end_of_day
51
32
  end
52
33
 
53
- pay_subscription = pay_customer.subscriptions.find_by(processor_id: object.id)
54
- if pay_subscription
34
+ if (pay_subscription = find_by(customer: pay_customer, processor_id: object.id))
55
35
  pay_subscription.with_lock { pay_subscription.update!(attributes) }
56
36
  else
57
37
  name ||= Pay.default_product_name
58
- pay_subscription = pay_customer.subscriptions.create!(attributes.merge(name: name, processor_id: object.id))
38
+ create!(attributes.merge(customer: pay_customer, name: name, processor_id: object.id))
59
39
  end
60
-
61
- pay_subscription
62
40
  rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
63
41
  try += 1
64
42
  if try <= retries
@@ -69,11 +47,7 @@ module Pay
69
47
  end
70
48
  end
71
49
 
72
- def initialize(pay_subscription)
73
- @pay_subscription = pay_subscription
74
- end
75
-
76
- def subscription(**options)
50
+ def api_record(**options)
77
51
  gateway.subscription.find(processor_id)
78
52
  end
79
53
 
@@ -84,11 +58,9 @@ module Pay
84
58
  result = if on_trial?
85
59
  gateway.subscription.cancel(processor_id)
86
60
  else
87
- gateway.subscription.update(subscription.id, {
88
- number_of_billing_cycles: subscription.current_billing_cycle
89
- })
61
+ gateway.subscription.update(processor_id, {number_of_billing_cycles: api_record.current_billing_cycle})
90
62
  end
91
- pay_subscription.sync!(object: result.subscription)
63
+ sync!(object: result.subscription)
92
64
  rescue ::Braintree::BraintreeError => e
93
65
  raise Pay::Braintree::Error, e
94
66
  end
@@ -97,7 +69,7 @@ module Pay
97
69
  return if canceled?
98
70
 
99
71
  result = gateway.subscription.cancel(processor_id)
100
- pay_subscription.sync!(object: result.subscription)
72
+ sync!(object: result.subscription)
101
73
  rescue ::Braintree::BraintreeError => e
102
74
  raise Pay::Braintree::Error, e
103
75
  end
@@ -106,10 +78,6 @@ module Pay
106
78
  raise NotImplementedError, "Braintree does not support setting quantity on subscriptions"
107
79
  end
108
80
 
109
- def on_grace_period?
110
- ends_at? && ends_at > Time.current
111
- end
112
-
113
81
  def paused?
114
82
  false
115
83
  end
@@ -124,7 +92,7 @@ module Pay
124
92
 
125
93
  def resume
126
94
  unless resumable?
127
- raise StandardError, "You can only resume subscriptions within their grace period."
95
+ raise Error, "You can only resume subscriptions within their grace period."
128
96
  end
129
97
 
130
98
  if canceled? && on_trial?
@@ -138,15 +106,13 @@ module Pay
138
106
  trial_duration_unit: :day
139
107
  )
140
108
  else
141
- subscription = processor_subscription
142
-
143
- gateway.subscription.update(subscription.id, {
109
+ gateway.subscription.update(processor_id, {
144
110
  never_expires: true,
145
111
  number_of_billing_cycles: nil
146
112
  })
147
113
  end
148
114
 
149
- pay_subscription.update(status: :active)
115
+ update(ends_at: nil, status: :active)
150
116
  rescue ::Braintree::BraintreeError => e
151
117
  raise Pay::Braintree::Error, e
152
118
  end
@@ -165,26 +131,25 @@ module Pay
165
131
  end
166
132
 
167
133
  braintree_plan = find_braintree_plan(plan)
134
+ prorate = options.fetch(:prorate) { true }
168
135
 
169
- if would_change_billing_frequency?(braintree_plan) && prorate?
136
+ if would_change_billing_frequency?(braintree_plan) && prorate
170
137
  swap_across_frequencies(braintree_plan)
171
138
  return
172
139
  end
173
140
 
174
- subscription = processor_subscription
175
-
176
- result = gateway.subscription.update(subscription.id, {
141
+ result = gateway.subscription.update(processor_id, {
177
142
  plan_id: braintree_plan.id,
178
143
  price: braintree_plan.price,
179
144
  never_expires: true,
180
145
  number_of_billing_cycles: nil,
181
146
  options: {
182
- prorate_charges: prorate?
147
+ prorate_charges: prorate
183
148
  }
184
149
  })
185
150
  raise Error, "Braintree failed to swap plans: #{result.message}" unless result.success?
186
151
 
187
- pay_subscription.update(processor_plan: plan, ends_at: nil, status: :active)
152
+ update(processor_plan: plan, ends_at: nil, status: :active)
188
153
  rescue ::Braintree::BraintreeError => e
189
154
  raise Pay::Braintree::Error, e
190
155
  end
@@ -198,7 +163,7 @@ module Pay
198
163
  )
199
164
 
200
165
  if result.success?
201
- pay_subscription.update(status: :active)
166
+ update(status: :active)
202
167
  end
203
168
  end
204
169
 
@@ -224,30 +189,25 @@ module Pay
224
189
 
225
190
  def discount_for_switching_to_monthly(current_plan, plan)
226
191
  cycles = (money_remaining_on_yearly_plan(current_plan) / plan.price).floor
227
- OpenStruct.new(
192
+ ActiveSupport::InheritableOptions.new(
228
193
  amount: plan.price,
229
194
  number_of_billing_cycles: cycles
230
195
  )
231
196
  end
232
197
 
233
198
  def money_remaining_on_yearly_plan(current_plan)
234
- end_date = processor_subscription.billing_period_end_date.to_date
199
+ end_date = api_record.billing_period_end_date.to_date
235
200
  (current_plan.price / 365) * (end_date - Date.today)
236
201
  end
237
202
 
238
- def discount_for_switching_to_yearly
239
- amount = 0
240
-
241
- processor_subscription.discounts.each do |discount|
203
+ def discount_for_switching_to_yearly(amount: 0)
204
+ api_record.discounts.each do |discount|
242
205
  if discount.id == "plan-credit"
243
206
  amount += discount.amount * discount.number_of_billing_cycles
244
207
  end
245
208
  end
246
209
 
247
- OpenStruct.new(
248
- amount: amount,
249
- number_of_billing_cycles: 1
250
- )
210
+ ActiveSupport::InheritableOptions.new(amount: amount, number_of_billing_cycles: 1)
251
211
  end
252
212
 
253
213
  def swap_across_frequencies(plan)
@@ -283,3 +243,5 @@ module Pay
283
243
  end
284
244
  end
285
245
  end
246
+
247
+ ActiveSupport.run_load_hooks :pay_braintree_subscription, Pay::Braintree::Subscription