pay 6.8.0 → 7.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/pay/webhooks/paddle_billing_controller.rb +51 -0
  3. data/app/controllers/pay/webhooks/{paddle_controller.rb → paddle_classic_controller.rb} +6 -6
  4. data/app/mailers/pay/application_mailer.rb +5 -1
  5. data/app/models/pay/charge.rb +1 -2
  6. data/app/models/pay/customer.rb +1 -2
  7. data/app/models/pay/merchant.rb +1 -3
  8. data/app/models/pay/payment_method.rb +1 -2
  9. data/app/models/pay/subscription.rb +10 -11
  10. data/app/models/pay/webhook.rb +10 -5
  11. data/app/views/pay/payments/show.html.erb +10 -17
  12. data/config/locales/en.yml +1 -1
  13. data/config/routes.rb +2 -1
  14. data/db/migrate/1_create_pay_tables.rb +6 -1
  15. data/lib/pay/braintree/subscription.rb +12 -2
  16. data/lib/pay/engine.rb +3 -2
  17. data/lib/pay/env.rb +1 -7
  18. data/lib/pay/fake_processor/subscription.rb +11 -1
  19. data/lib/pay/lemon_squeezy/billable.rb +90 -0
  20. data/lib/pay/lemon_squeezy/charge.rb +68 -0
  21. data/lib/pay/{paddle → lemon_squeezy}/error.rb +1 -1
  22. data/lib/pay/lemon_squeezy/payment_method.rb +40 -0
  23. data/lib/pay/lemon_squeezy/subscription.rb +185 -0
  24. data/lib/pay/lemon_squeezy/webhooks/subscription.rb +11 -0
  25. data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +11 -0
  26. data/lib/pay/lemon_squeezy.rb +138 -0
  27. data/lib/pay/paddle_billing/billable.rb +90 -0
  28. data/lib/pay/paddle_billing/charge.rb +68 -0
  29. data/lib/pay/paddle_billing/error.rb +7 -0
  30. data/lib/pay/paddle_billing/payment_method.rb +40 -0
  31. data/lib/pay/paddle_billing/subscription.rb +185 -0
  32. data/lib/pay/paddle_billing/webhooks/subscription.rb +11 -0
  33. data/lib/pay/paddle_billing/webhooks/transaction_completed.rb +11 -0
  34. data/lib/pay/paddle_billing.rb +58 -0
  35. data/lib/pay/{paddle → paddle_classic}/billable.rb +9 -10
  36. data/lib/pay/paddle_classic/charge.rb +35 -0
  37. data/lib/pay/paddle_classic/error.rb +7 -0
  38. data/lib/pay/{paddle → paddle_classic}/payment_method.rb +4 -4
  39. data/lib/pay/{paddle → paddle_classic}/subscription.rb +39 -30
  40. data/lib/pay/{paddle → paddle_classic}/webhooks/signature_verifier.rb +4 -4
  41. data/lib/pay/{paddle → paddle_classic}/webhooks/subscription_cancelled.rb +5 -4
  42. data/lib/pay/{paddle → paddle_classic}/webhooks/subscription_created.rb +2 -2
  43. data/lib/pay/{paddle → paddle_classic}/webhooks/subscription_payment_refunded.rb +2 -2
  44. data/lib/pay/{paddle → paddle_classic}/webhooks/subscription_payment_succeeded.rb +7 -7
  45. data/lib/pay/{paddle → paddle_classic}/webhooks/subscription_updated.rb +2 -2
  46. data/lib/pay/paddle_classic.rb +82 -0
  47. data/lib/pay/receipts.rb +1 -1
  48. data/lib/pay/stripe/billable.rb +6 -2
  49. data/lib/pay/stripe/charge.rb +8 -4
  50. data/lib/pay/stripe/payment_method.rb +9 -1
  51. data/lib/pay/stripe/subscription.rb +54 -4
  52. data/lib/pay/stripe.rb +3 -4
  53. data/lib/pay/version.rb +1 -1
  54. data/lib/pay.rb +3 -2
  55. data/lib/tasks/pay.rake +2 -2
  56. metadata +33 -17
  57. data/lib/pay/paddle/charge.rb +0 -35
  58. data/lib/pay/paddle/response.rb +0 -0
  59. data/lib/pay/paddle.rb +0 -80
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de3a99204ee0e081f59774da0369b8e820a8d1fb33d810871dbc62ec009012c1
4
- data.tar.gz: 19bbcd3c80230b270b2fc60338d410d6ceb37e2dc9466d1674f8552ecbdf46e9
3
+ metadata.gz: bcb0963ca95179e0b56cf07bfdd56e0341d2ca2ae52fc6cb65014251bc62f3a3
4
+ data.tar.gz: 9970fc734d34bacb0c35dfe327c59e80e906ed5a8d702c5feed4c68fbd5b321a
5
5
  SHA512:
6
- metadata.gz: 0f504f26b84e856d1fd241e61537f1203539b76f58f9af69a768316e97e710ff24b5a79ac2179a1c335d5691283dc9e097476e696b1551ec212046f8dd97f026
7
- data.tar.gz: 87e4ae3cc89868ed3faa76e3a73b0aaba568c87d33e7c1c003619c4fc69887edcce7fab67f3c40cb57924423a4f9c0b2328cebf1491164ae975c79f586fa7cb9
6
+ metadata.gz: fec7e0d0f3ad62cbcc21b533680041176280035e0ed2a0f88ef5e871535cff60f5b8965b0ce0687220c66610e3a121aa52e1dcc00f97c416512a3f0a446854a1
7
+ data.tar.gz: cc039d575168e1cb8f31f331ce6baddf063b69f150dab928d51293c10a0c3a61eb962598885b8be72eb3122cf557360a76f88bc1f351f965c9ce0bf8727e75ed
@@ -0,0 +1,51 @@
1
+ module Pay
2
+ module Webhooks
3
+ class PaddleBillingController < 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["Paddle-Signature"])
10
+ queue_event(verify_params.as_json)
11
+ head :ok
12
+ else
13
+ head :bad_request
14
+ end
15
+ rescue Pay::PaddleBilling::Error
16
+ head :bad_request
17
+ end
18
+
19
+ private
20
+
21
+ def queue_event(event)
22
+ return unless Pay::Webhooks.delegator.listening?("paddle_billing.#{params[:event_type]}")
23
+
24
+ record = Pay::Webhook.create!(processor: :paddle_billing, event_type: params[:event_type], event: event)
25
+ Pay::Webhooks::ProcessJob.perform_later(record)
26
+ end
27
+
28
+ # Pass Paddle signature from request.headers["Paddle-Signature"]
29
+ def valid_signature?(paddle_signature)
30
+ return false if paddle_signature.blank?
31
+
32
+ ts_part, h1_part = paddle_signature.split(";")
33
+ _, ts = ts_part.split("=")
34
+ _, h1 = h1_part.split("=")
35
+
36
+ signed_payload = "#{ts}:#{request.raw_post}"
37
+
38
+ key = Pay::PaddleBilling.signing_secret
39
+ data = signed_payload
40
+ digest = OpenSSL::Digest.new("sha256")
41
+
42
+ hmac = OpenSSL::HMAC.hexdigest(digest, key, data)
43
+ hmac == h1
44
+ end
45
+
46
+ def verify_params
47
+ params.except(:action, :controller).permit!
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,6 +1,6 @@
1
1
  module Pay
2
2
  module Webhooks
3
- class PaddleController < Pay::ApplicationController
3
+ class PaddleClassicController < Pay::ApplicationController
4
4
  if Rails.application.config.action_controller.default_protect_from_forgery
5
5
  skip_before_action :verify_authenticity_token
6
6
  end
@@ -8,24 +8,24 @@ module Pay
8
8
  def create
9
9
  queue_event(verified_event)
10
10
  head :ok
11
- rescue Pay::Paddle::Error
11
+ rescue Pay::PaddleClassic::Error
12
12
  head :bad_request
13
13
  end
14
14
 
15
15
  private
16
16
 
17
17
  def queue_event(event)
18
- return unless Pay::Webhooks.delegator.listening?("paddle.#{params[:alert_name]}")
18
+ return unless Pay::Webhooks.delegator.listening?("paddle_classic.#{params[:alert_name]}")
19
19
 
20
- record = Pay::Webhook.create!(processor: :paddle, event_type: params[:alert_name], event: event)
20
+ record = Pay::Webhook.create!(processor: :paddle_classic, event_type: params[:alert_name], event: event)
21
21
  Pay::Webhooks::ProcessJob.perform_later(record)
22
22
  end
23
23
 
24
24
  def verified_event
25
25
  event = verify_params.as_json
26
- verifier = Pay::Paddle::Webhooks::SignatureVerifier.new(event)
26
+ verifier = Pay::PaddleClassic::Webhooks::SignatureVerifier.new(event)
27
27
  return event if verifier.verify
28
- raise Pay::Paddle::Error, "Unable to verify Paddle webhook event"
28
+ raise Pay::PaddleClassic::Error, "Unable to verify Paddle webhook event"
29
29
  end
30
30
 
31
31
  def verify_params
@@ -1,6 +1,10 @@
1
1
  module Pay
2
2
  class ApplicationMailer < ActionMailer::Base
3
- default from: Pay.support_email || ApplicationMailer.default_params[:from]
3
+ def self.default_from_address
4
+ Pay.support_email || ::ApplicationMailer.default_params[:from]
5
+ end
6
+
7
+ default from: default_from_address
4
8
  layout "mailer"
5
9
  end
6
10
  end
@@ -18,7 +18,6 @@ module Pay
18
18
  # Store the payment method kind (card, paypal, etc)
19
19
  store_accessor :data, :paddle_receipt_url
20
20
  store_accessor :data, :stripe_receipt_url
21
- store_accessor :data, :stripe_account
22
21
 
23
22
  # Payment method attributes
24
23
  store_accessor :data, :payment_method_type # card, paypal, sepa, etc
@@ -45,7 +44,7 @@ module Pay
45
44
  store_accessor :data, :refunds # array of refunds
46
45
 
47
46
  # Helpers for payment processors
48
- %w[braintree stripe paddle fake_processor].each do |processor_name|
47
+ %w[braintree stripe paddle_billing paddle_classic fake_processor].each do |processor_name|
49
48
  define_method "#{processor_name}?" do
50
49
  customer.processor == processor_name
51
50
  end
@@ -16,7 +16,6 @@ module Pay
16
16
  attribute :payment_method_token, :string
17
17
 
18
18
  # Account(s) for marketplace payments
19
- store_accessor :data, :stripe_account
20
19
  store_accessor :data, :braintree_account
21
20
 
22
21
  # Stripe invoice credit balance is a Hash-like object { "usd" => 1234 }
@@ -26,7 +25,7 @@ module Pay
26
25
  delegate :email, to: :owner
27
26
  delegate_missing_to :pay_processor
28
27
 
29
- %w[stripe braintree paddle fake_processor].each do |processor_name|
28
+ %w[stripe braintree paddle_billing paddle_classic fake_processor].each do |processor_name|
30
29
  scope processor_name, -> { where(processor: processor_name) }
31
30
 
32
31
  define_method "#{processor_name}?" do
@@ -18,9 +18,7 @@ module Pay
18
18
  end
19
19
 
20
20
  def onboarding_complete?
21
- ActiveModel::Type::Boolean.new.cast(
22
- (data.presence || {})["onboarding_complete"]
23
- )
21
+ ActiveModel::Type::Boolean.new.cast(data&.fetch("onboarding_complete")) || false
24
22
  end
25
23
  end
26
24
  end
@@ -4,12 +4,11 @@ module Pay
4
4
 
5
5
  belongs_to :customer
6
6
 
7
- store_accessor :data, :stripe_account
8
7
  store_accessor :data, :brand # Visa, Mastercard, Discover, PayPal
9
8
  store_accessor :data, :last4
10
9
  store_accessor :data, :exp_month
11
10
  store_accessor :data, :exp_year
12
- store_accessor :data, :email # PayPal email, etc
11
+ store_accessor :data, :email # PayPal, Stripe Link, etc
13
12
  store_accessor :data, :username
14
13
  store_accessor :data, :bank
15
14
 
@@ -4,14 +4,16 @@ module Pay
4
4
 
5
5
  # Associations
6
6
  belongs_to :customer
7
+ belongs_to :payment_method, optional: true, primary_key: :processor_id
7
8
  has_many :charges
8
9
 
9
10
  # Scopes
10
11
  scope :for_name, ->(name) { where(name: name) }
11
- scope :on_trial, -> { where.not(trial_ends_at: nil).where("#{table_name}.trial_ends_at > ?", Time.current) }
12
- scope :cancelled, -> { where.not(ends_at: nil) }
13
- scope :on_grace_period, -> { cancelled.where("#{table_name}.ends_at > ?", Time.current) }
14
- scope :active, -> { where(status: ["trialing", "active", "canceled"], ends_at: nil).pause_not_started.or(on_grace_period).or(on_trial) }
12
+ scope :on_trial, -> { where("trial_ends_at > ?", Time.current) }
13
+ scope :canceled, -> { where.not(ends_at: nil) }
14
+ scope :cancelled, -> { canceled }
15
+ scope :on_grace_period, -> { where("#{table_name}.ends_at IS NOT NULL AND #{table_name}.ends_at > ?", Time.current) }
16
+ scope :active, -> { where(status: ["trialing", "active"]).pause_not_started.where("#{table_name}.ends_at IS NULL OR #{table_name}.ends_at > ?", Time.current).where("trial_ends_at IS NULL OR trial_ends_at > ?", Time.current) }
15
17
  scope :paused, -> { where(status: "paused").or(where("pause_starts_at <= ?", Time.current)) }
16
18
  scope :pause_not_started, -> { where("pause_starts_at IS NULL OR pause_starts_at > ?", Time.current) }
17
19
  scope :active_or_paused, -> { active.or(paused) }
@@ -27,7 +29,6 @@ module Pay
27
29
 
28
30
  store_accessor :data, :paddle_update_url
29
31
  store_accessor :data, :paddle_cancel_url
30
- store_accessor :data, :stripe_account
31
32
  store_accessor :data, :subscription_items
32
33
 
33
34
  attribute :prorate, :boolean, default: true
@@ -42,7 +43,7 @@ module Pay
42
43
  delegate_missing_to :payment_processor
43
44
 
44
45
  # Helper methods for payment processors
45
- %w[braintree stripe paddle fake_processor].each do |processor_name|
46
+ %w[braintree stripe paddle_billing paddle_classic fake_processor].each do |processor_name|
46
47
  define_method "#{processor_name}?" do
47
48
  customer.processor == processor_name
48
49
  end
@@ -106,9 +107,9 @@ module Pay
106
107
 
107
108
  # If you cancel during a trial, you should still retain access until the end of the trial
108
109
  # Otherwise a subscription is active unless it has ended or is currently paused
109
- # Check the subscription status so we don't accidentally consider "incomplete", "past_due", or other statuses as active
110
+ # Check the subscription status so we don't accidentally consider "incomplete", "unpaid", or other statuses as active
110
111
  def active?
111
- ["trialing", "active", "canceled"].include?(status) &&
112
+ ["trialing", "active"].include?(status) &&
112
113
  (!(canceled? || paused?) || on_trial? || on_grace_period?)
113
114
  end
114
115
 
@@ -160,9 +161,7 @@ module Pay
160
161
  private
161
162
 
162
163
  def cancel_if_active
163
- if active?
164
- cancel_now!
165
- end
164
+ cancel_now! if active?
166
165
  rescue => e
167
166
  Rails.logger.info "[Pay] Unable to automatically cancel subscription `#{customer.processor} #{id}`: #{e.message}"
168
167
  end
@@ -17,7 +17,9 @@ module Pay
17
17
  case processor
18
18
  when "braintree"
19
19
  Pay.braintree_gateway.webhook_notification.parse(event["bt_signature"], event["bt_payload"])
20
- when "paddle"
20
+ when "paddle_billing"
21
+ to_recursive_ostruct(event["data"])
22
+ when "paddle_classic"
21
23
  to_recursive_ostruct(event)
22
24
  when "stripe"
23
25
  ::Stripe::Event.construct_from(event)
@@ -26,11 +28,14 @@ module Pay
26
28
  end
27
29
  end
28
30
 
29
- def to_recursive_ostruct(hash)
30
- result = hash.each_with_object({}) do |(key, val), memo|
31
- memo[key] = val.is_a?(Hash) ? to_recursive_ostruct(val) : val
31
+ def to_recursive_ostruct(obj)
32
+ if obj.is_a?(Hash)
33
+ OpenStruct.new(obj.map { |key, val| [key, to_recursive_ostruct(val)] }.to_h)
34
+ elsif obj.is_a?(Array)
35
+ obj.map { |o| to_recursive_ostruct(o) }
36
+ else # Assumed to be a primitive value
37
+ obj
32
38
  end
33
- OpenStruct.new(result)
34
39
  end
35
40
  end
36
41
  end
@@ -64,7 +64,7 @@
64
64
  </div>
65
65
 
66
66
  <script type="module">
67
- window.stripe = Stripe('<%= Pay::Stripe.public_key %>');
67
+ window.stripe = Stripe('<%= Pay::Stripe.public_key %>')
68
68
 
69
69
  import { Application, Controller } from 'https://unpkg.com/@hotwired/stimulus/dist/stimulus.js'
70
70
  const application = Application.start()
@@ -83,34 +83,27 @@
83
83
 
84
84
  connect() {
85
85
  if (this.hasCardTarget) {
86
- this.elements = stripe.elements()
87
- this.cardElement = this.elements.create("card")
88
- this.cardElement.mount(this.cardTarget)
86
+ this.elements = stripe.elements({clientSecret: this.clientSecretValue})
87
+ this.payment = this.elements.create('payment')
88
+ this.payment.mount(this.cardTarget)
89
89
  }
90
90
  }
91
91
 
92
92
  statusValueChanged() {
93
93
  switch(this.statusValue) {
94
94
  case "requires_action":
95
- stripe.confirmCardPayment(this.clientSecretValue).then(this.handleConfirmResult.bind(this))
96
- break;
95
+ this.processingValue = true
96
+ stripe.confirmPayment({clientSecret: this.clientSecretValue, confirmParams: { return_url: document.location.href }}).then(this.handleConfirmResult.bind(this))
97
+ break
97
98
  case "requires_payment_method":
98
99
  this.cardFieldsTarget.classList.toggle("hidden", false)
99
- break;
100
+ break
100
101
  }
101
102
  }
102
103
 
103
104
  confirmPayment() {
104
105
  this.processingValue = true
105
- this.completeValue = false
106
- stripe.confirmCardPayment(this.clientSecretValue, {
107
- payment_method: {
108
- card: this.cardElement,
109
- billing_details: { name: this.nameTarget.value }
110
- },
111
- save_payment_method: this.customerValue != "",
112
- setup_future_usage: 'off_session',
113
- }).then(this.handleConfirmResult.bind(this))
106
+ stripe.confirmPayment({elements: this.elements, confirmParams: { return_url: document.location.href }}).then(this.handleConfirmResult.bind(this))
114
107
  }
115
108
 
116
109
  handleConfirmResult(result) {
@@ -125,7 +118,7 @@
125
118
  this.statusValue = result.error.payment_intent.status
126
119
  }
127
120
  } else {
128
- this.completeValue = true;
121
+ this.completeValue = true
129
122
  this.successMessageValue = '<%=t "pay.requires_action.success" %>'
130
123
  }
131
124
  }
@@ -2,7 +2,7 @@ en:
2
2
  pay:
3
3
  successful:
4
4
  header: "Payment Successful"
5
- description: "This payment was already successfully confirmed."
5
+ description: "This payment was successfully confirmed."
6
6
  cancelled:
7
7
  header: "Payment Cancelled"
8
8
  description: "This payment was cancelled."
data/config/routes.rb CHANGED
@@ -4,5 +4,6 @@ Pay::Engine.routes.draw do
4
4
  resources :payments, only: [:show], module: :pay
5
5
  post "webhooks/stripe", to: "pay/webhooks/stripe#create" if Pay::Stripe.enabled?
6
6
  post "webhooks/braintree", to: "pay/webhooks/braintree#create" if Pay::Braintree.enabled?
7
- post "webhooks/paddle", to: "pay/webhooks/paddle#create" if Pay::Paddle.enabled?
7
+ post "webhooks/paddle_billing", to: "pay/webhooks/paddle_billing#create" if Pay::PaddleBilling.enabled?
8
+ post "webhooks/paddle_classic", to: "pay/webhooks/paddle_classic#create" if Pay::PaddleClassic.enabled?
8
9
  end
@@ -8,10 +8,11 @@ class CreatePayTables < ActiveRecord::Migration[6.0]
8
8
  t.string :processor_id
9
9
  t.boolean :default
10
10
  t.public_send Pay::Adapter.json_column_type, :data
11
+ t.string :stripe_account
11
12
  t.datetime :deleted_at
12
13
  t.timestamps
13
14
  end
14
- add_index :pay_customers, [:owner_type, :owner_id, :deleted_at, :default], name: :pay_customer_owner_index
15
+ add_index :pay_customers, [:owner_type, :owner_id, :deleted_at], name: :pay_customer_owner_index, unique: true
15
16
  add_index :pay_customers, [:processor, :processor_id], unique: true
16
17
 
17
18
  create_table :pay_merchants, id: primary_key_type do |t|
@@ -30,6 +31,7 @@ class CreatePayTables < ActiveRecord::Migration[6.0]
30
31
  t.boolean :default
31
32
  t.string :type
32
33
  t.public_send Pay::Adapter.json_column_type, :data
34
+ t.string :stripe_account
33
35
  t.timestamps
34
36
  end
35
37
  add_index :pay_payment_methods, [:customer_id, :processor_id], unique: true
@@ -52,6 +54,8 @@ class CreatePayTables < ActiveRecord::Migration[6.0]
52
54
  t.decimal :application_fee_percent, precision: 8, scale: 2
53
55
  t.public_send Pay::Adapter.json_column_type, :metadata
54
56
  t.public_send Pay::Adapter.json_column_type, :data
57
+ t.string :stripe_account
58
+ t.string :payment_method_id
55
59
  t.timestamps
56
60
  end
57
61
  add_index :pay_subscriptions, [:customer_id, :processor_id], unique: true
@@ -68,6 +72,7 @@ class CreatePayTables < ActiveRecord::Migration[6.0]
68
72
  t.integer :amount_refunded
69
73
  t.public_send Pay::Adapter.json_column_type, :metadata
70
74
  t.public_send Pay::Adapter.json_column_type, :data
75
+ t.string :stripe_account
71
76
  t.timestamps
72
77
  end
73
78
  add_index :pay_charges, [:customer_id, :processor_id], unique: true
@@ -35,8 +35,9 @@ module Pay
35
35
  created_at: object.created_at,
36
36
  current_period_end: object.billing_period_end_date,
37
37
  current_period_start: object.billing_period_start_date,
38
+ payment_method_id: object.payment_method_token,
38
39
  processor_plan: object.plan_id,
39
- status: object.status.underscore,
40
+ status: object.status.parameterize(separator: "_"),
40
41
  trial_ends_at: (object.created_at + object.trial_duration.send(object.trial_duration_unit) if object.trial_period)
41
42
  }
42
43
 
@@ -77,6 +78,9 @@ module Pay
77
78
  end
78
79
 
79
80
  def cancel(**options)
81
+ return if canceled?
82
+
83
+ # Braintree doesn't allow canceling at period end while on trial, so trials are canceled immediately
80
84
  result = if on_trial?
81
85
  gateway.subscription.cancel(processor_id)
82
86
  else
@@ -90,6 +94,8 @@ module Pay
90
94
  end
91
95
 
92
96
  def cancel_now!(**options)
97
+ return if canceled?
98
+
93
99
  result = gateway.subscription.cancel(processor_id)
94
100
  pay_subscription.sync!(object: result.subscription)
95
101
  rescue ::Braintree::BraintreeError => e
@@ -112,8 +118,12 @@ module Pay
112
118
  raise NotImplementedError, "Braintree does not support pausing subscriptions"
113
119
  end
114
120
 
121
+ def resumable?
122
+ on_grace_period?
123
+ end
124
+
115
125
  def resume
116
- unless on_grace_period?
126
+ unless resumable?
117
127
  raise StandardError, "You can only resume subscriptions within their grace period."
118
128
  end
119
129
 
data/lib/pay/engine.rb CHANGED
@@ -26,13 +26,14 @@ module Pay
26
26
  config.before_initialize do
27
27
  Pay::Stripe.configure_webhooks if Pay::Stripe.enabled?
28
28
  Pay::Braintree.configure_webhooks if Pay::Braintree.enabled?
29
- Pay::Paddle.configure_webhooks if Pay::Paddle.enabled?
29
+ Pay::PaddleBilling.configure_webhooks if Pay::PaddleBilling.enabled?
30
+ Pay::PaddleClassic.configure_webhooks if Pay::PaddleClassic.enabled?
30
31
  end
31
32
 
32
33
  config.to_prepare do
33
34
  Pay::Stripe.setup if Pay::Stripe.enabled?
34
35
  Pay::Braintree.setup if Pay::Braintree.enabled?
35
- Pay::Paddle.setup if Pay::Paddle.enabled?
36
+ Pay::PaddleBilling.setup if Pay::PaddleBilling.enabled?
36
37
 
37
38
  if defined?(::Receipts::VERSION)
38
39
  if Pay::Engine.version_matches?(required: "~> 2", current: ::Receipts::VERSION)
data/lib/pay/env.rb CHANGED
@@ -21,9 +21,7 @@ module Pay
21
21
  def find_value_by_name(scope, name)
22
22
  ENV["#{scope.upcase}_#{name.upcase}"] ||
23
23
  credentials&.dig(env, scope, name) ||
24
- credentials&.dig(scope, name) ||
25
- secrets&.dig(env, scope, name) ||
26
- secrets&.dig(scope, name)
24
+ credentials&.dig(scope, name)
27
25
  rescue ActiveSupport::MessageEncryptor::InvalidMessage
28
26
  Rails.logger.error <<~MESSAGE
29
27
  Rails was unable to decrypt credentials. Pay checks the Rails credentials to look for API keys for payment processors.
@@ -38,10 +36,6 @@ module Pay
38
36
  Rails.env.to_sym
39
37
  end
40
38
 
41
- def secrets
42
- Rails.application.secrets if Rails.application.respond_to?(:secrets)
43
- end
44
-
45
39
  def credentials
46
40
  Rails.application.credentials if Rails.application.respond_to?(:credentials)
47
41
  end
@@ -29,6 +29,8 @@ module Pay
29
29
  # With trial, sets end to trial end (mimicing Stripe)
30
30
  # Without trial, sets can ends_at to end of month
31
31
  def cancel(**options)
32
+ return if canceled?
33
+
32
34
  if pay_subscription.on_trial?
33
35
  pay_subscription.update(ends_at: pay_subscription.trial_ends_at)
34
36
  else
@@ -37,6 +39,8 @@ module Pay
37
39
  end
38
40
 
39
41
  def cancel_now!(**options)
42
+ return if canceled?
43
+
40
44
  ends_at = Time.current
41
45
  pay_subscription.update(
42
46
  status: :canceled,
@@ -61,10 +65,16 @@ module Pay
61
65
  pay_subscription.update(status: :paused, trial_ends_at: Time.current)
62
66
  end
63
67
 
68
+ def resumable?
69
+ on_grace_period? || paused?
70
+ end
71
+
64
72
  def resume
65
- unless on_grace_period? || paused?
73
+ unless resumable?
66
74
  raise StandardError, "You can only resume subscriptions within their grace period."
67
75
  end
76
+
77
+ pay_subscription.update(status: :active, trial_ends_at: nil, ends_at: nil)
68
78
  end
69
79
 
70
80
  def swap(plan, **options)
@@ -0,0 +1,90 @@
1
+ module Pay
2
+ module PaddleBilling
3
+ class Billable
4
+ attr_reader :pay_customer
5
+
6
+ delegate :processor_id,
7
+ :processor_id?,
8
+ :email,
9
+ :customer_name,
10
+ :card_token,
11
+ to: :pay_customer
12
+
13
+ def initialize(pay_customer)
14
+ @pay_customer = pay_customer
15
+ end
16
+
17
+ def customer_attributes
18
+ {email: email, name: customer_name}
19
+ end
20
+
21
+ # Retrieves a Paddle::Customer object
22
+ #
23
+ # Finds an existing Paddle::Customer if processor_id exists
24
+ # Creates a new Paddle::Customer using `email` and `customer_name` if empty processor_id
25
+ #
26
+ # Returns a Paddle::Customer object
27
+ def customer
28
+ if processor_id?
29
+ ::Paddle::Customer.retrieve(id: processor_id)
30
+ else
31
+ sc = ::Paddle::Customer.create(email: email, name: customer_name)
32
+ pay_customer.update!(processor_id: sc.id)
33
+ sc
34
+ end
35
+ rescue ::Paddle::Error => e
36
+ raise Pay::PaddleBilling::Error, e
37
+ end
38
+
39
+ # Syncs name and email to Paddle::Customer
40
+ # 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)
45
+ end
46
+
47
+ def charge(amount, options = {})
48
+ return Pay::Error unless options
49
+
50
+ items = options[:items]
51
+ opts = options.except(:items).merge(customer_id: processor_id)
52
+ transaction = ::Paddle::Transaction.create(items: items, **opts)
53
+
54
+ attrs = {
55
+ amount: transaction.details.totals.grand_total,
56
+ created_at: transaction.created_at,
57
+ currency: transaction.currency_code,
58
+ metadata: transaction.details.line_items&.first&.id
59
+ }
60
+
61
+ charge = pay_customer.charges.find_or_initialize_by(processor_id: transaction.id)
62
+ charge.update(attrs)
63
+ charge
64
+ rescue ::Paddle::Error => e
65
+ raise Pay::PaddleBilling::Error, e
66
+ end
67
+
68
+ def subscribe(name: Pay.default_product_name, plan: Pay.default_plan_name, **options)
69
+ # pass
70
+ end
71
+
72
+ # Paddle does not use payment method tokens. The method signature has it here
73
+ # to have a uniform API with the other payment processors.
74
+ 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
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,68 @@
1
+ module Pay
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
11
+
12
+ def self.sync(charge_id, object: nil, try: 0, retries: 1)
13
+ # Skip loading the latest charge details from the API if we already have it
14
+ object ||= ::Paddle::Transaction.retrieve(id: charge_id)
15
+
16
+ # Ignore transactions that aren't completed
17
+ return unless object.status == "completed"
18
+
19
+ # Ignore charges without a Customer
20
+ return if object.customer_id.blank?
21
+
22
+ pay_customer = Pay::Customer.find_by(processor: :paddle_billing, processor_id: object.customer_id)
23
+ return unless pay_customer
24
+
25
+ # Ignore transactions that are payment method changes
26
+ # But update the customer's payment method
27
+ if object.origin == "subscription_payment_method_change"
28
+ Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
29
+ return
30
+ end
31
+
32
+ attrs = {
33
+ amount: object.details.totals.grand_total,
34
+ created_at: object.created_at,
35
+ currency: object.currency_code,
36
+ metadata: object.details.line_items&.first&.id,
37
+ subscription: pay_customer.subscriptions.find_by(processor_id: object.subscription_id)
38
+ }
39
+
40
+ if object.payment
41
+ case object.payment.method_details.type.downcase
42
+ when "card"
43
+ attrs[:payment_method_type] = "card"
44
+ attrs[:brand] = details.card.type
45
+ attrs[:exp_month] = details.card.expiry_month
46
+ attrs[:exp_year] = details.card.expiry_year
47
+ attrs[:last4] = details.card.last4
48
+ when "paypal"
49
+ attrs[:payment_method_type] = "paypal"
50
+ end
51
+
52
+ # Update customer's payment method
53
+ Pay::PaddleBilling::PaymentMethod.sync(pay_customer: pay_customer, attributes: object.payments.first)
54
+ end
55
+
56
+ # 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
61
+ pay_charge
62
+ else
63
+ pay_customer.charges.create!(attrs.merge(processor_id: object.id))
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end