pay 6.8.0 → 7.0.0

Sign up to get free protection for your applications and to get access to all the features.
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