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