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.
- checksums.yaml +4 -4
- data/app/controllers/pay/webhooks/paddle_billing_controller.rb +51 -0
- data/app/controllers/pay/webhooks/{paddle_controller.rb → paddle_classic_controller.rb} +6 -6
- data/app/mailers/pay/application_mailer.rb +5 -1
- data/app/models/pay/charge.rb +1 -2
- data/app/models/pay/customer.rb +1 -2
- data/app/models/pay/merchant.rb +1 -3
- data/app/models/pay/payment_method.rb +1 -2
- data/app/models/pay/subscription.rb +10 -11
- data/app/models/pay/webhook.rb +10 -5
- data/app/views/pay/payments/show.html.erb +10 -17
- data/config/locales/en.yml +1 -1
- data/config/routes.rb +2 -1
- data/db/migrate/1_create_pay_tables.rb +6 -1
- data/lib/pay/braintree/subscription.rb +12 -2
- data/lib/pay/engine.rb +3 -2
- data/lib/pay/env.rb +1 -7
- data/lib/pay/fake_processor/subscription.rb +11 -1
- data/lib/pay/lemon_squeezy/billable.rb +90 -0
- data/lib/pay/lemon_squeezy/charge.rb +68 -0
- data/lib/pay/{paddle → lemon_squeezy}/error.rb +1 -1
- data/lib/pay/lemon_squeezy/payment_method.rb +40 -0
- data/lib/pay/lemon_squeezy/subscription.rb +185 -0
- data/lib/pay/lemon_squeezy/webhooks/subscription.rb +11 -0
- data/lib/pay/lemon_squeezy/webhooks/transaction_completed.rb +11 -0
- data/lib/pay/lemon_squeezy.rb +138 -0
- data/lib/pay/paddle_billing/billable.rb +90 -0
- data/lib/pay/paddle_billing/charge.rb +68 -0
- data/lib/pay/paddle_billing/error.rb +7 -0
- data/lib/pay/paddle_billing/payment_method.rb +40 -0
- data/lib/pay/paddle_billing/subscription.rb +185 -0
- data/lib/pay/paddle_billing/webhooks/subscription.rb +11 -0
- data/lib/pay/paddle_billing/webhooks/transaction_completed.rb +11 -0
- data/lib/pay/paddle_billing.rb +58 -0
- data/lib/pay/{paddle → paddle_classic}/billable.rb +9 -10
- data/lib/pay/paddle_classic/charge.rb +35 -0
- data/lib/pay/paddle_classic/error.rb +7 -0
- data/lib/pay/{paddle → paddle_classic}/payment_method.rb +4 -4
- data/lib/pay/{paddle → paddle_classic}/subscription.rb +39 -30
- data/lib/pay/{paddle → paddle_classic}/webhooks/signature_verifier.rb +4 -4
- data/lib/pay/{paddle → paddle_classic}/webhooks/subscription_cancelled.rb +5 -4
- data/lib/pay/{paddle → paddle_classic}/webhooks/subscription_created.rb +2 -2
- data/lib/pay/{paddle → paddle_classic}/webhooks/subscription_payment_refunded.rb +2 -2
- data/lib/pay/{paddle → paddle_classic}/webhooks/subscription_payment_succeeded.rb +7 -7
- data/lib/pay/{paddle → paddle_classic}/webhooks/subscription_updated.rb +2 -2
- data/lib/pay/paddle_classic.rb +82 -0
- data/lib/pay/receipts.rb +1 -1
- data/lib/pay/stripe/billable.rb +6 -2
- data/lib/pay/stripe/charge.rb +8 -4
- data/lib/pay/stripe/payment_method.rb +9 -1
- data/lib/pay/stripe/subscription.rb +54 -4
- data/lib/pay/stripe.rb +3 -4
- data/lib/pay/version.rb +1 -1
- data/lib/pay.rb +3 -2
- data/lib/tasks/pay.rake +2 -2
- metadata +33 -17
- data/lib/pay/paddle/charge.rb +0 -35
- data/lib/pay/paddle/response.rb +0 -0
- data/lib/pay/paddle.rb +0 -80
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bcb0963ca95179e0b56cf07bfdd56e0341d2ca2ae52fc6cb65014251bc62f3a3
|
4
|
+
data.tar.gz: 9970fc734d34bacb0c35dfe327c59e80e906ed5a8d702c5feed4c68fbd5b321a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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::
|
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?("
|
18
|
+
return unless Pay::Webhooks.delegator.listening?("paddle_classic.#{params[:alert_name]}")
|
19
19
|
|
20
|
-
record = Pay::Webhook.create!(processor: :
|
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::
|
26
|
+
verifier = Pay::PaddleClassic::Webhooks::SignatureVerifier.new(event)
|
27
27
|
return event if verifier.verify
|
28
|
-
raise Pay::
|
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
|
-
|
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
|
data/app/models/pay/charge.rb
CHANGED
@@ -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
|
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
|
data/app/models/pay/customer.rb
CHANGED
@@ -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
|
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
|
data/app/models/pay/merchant.rb
CHANGED
@@ -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
|
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
|
12
|
-
scope :
|
13
|
-
scope :
|
14
|
-
scope :
|
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
|
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", "
|
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"
|
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
|
data/app/models/pay/webhook.rb
CHANGED
@@ -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 "
|
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(
|
30
|
-
|
31
|
-
|
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.
|
88
|
-
this.
|
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
|
-
|
96
|
-
|
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.
|
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
|
}
|
data/config/locales/en.yml
CHANGED
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/
|
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
|
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.
|
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
|
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::
|
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::
|
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
|
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
|