pay 12.3.0 → 12.4.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/mailers/pay/user_mailer.rb +7 -2
- data/app/models/pay/charge.rb +12 -5
- data/app/models/pay/customer.rb +12 -5
- data/app/models/pay/lemon_squeezy/subscription.rb +1 -1
- data/app/models/pay/stripe/charge.rb +1 -1
- data/app/models/pay/subscription.rb +12 -5
- data/lib/pay/attributes.rb +1 -3
- data/lib/pay/braintree.rb +3 -13
- data/lib/pay/engine.rb +7 -5
- data/lib/pay/env.rb +41 -18
- data/lib/pay/lemon_squeezy.rb +2 -12
- data/lib/pay/paddle_billing.rb +5 -13
- data/lib/pay/paddle_classic.rb +15 -22
- data/lib/pay/stripe.rb +11 -22
- data/lib/pay/version.rb +3 -1
- data/lib/pay.rb +102 -49
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40ac78c02d095d82b2639d4755d68795de86c38bf281e125663268521c295fea
|
|
4
|
+
data.tar.gz: 7252f8657c37679b590863bc28a901ef6547bfdae139555a9a4d4fd8eb523f90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5bbc88551ef32e78ffe4d4b7ef10ccd03b319e5636157a1c887d235a25ff61bc9db218d305da92eda6c96358f74c575580998d9e63cd7ed0140630b865b874d6
|
|
7
|
+
data.tar.gz: 508cd0bef62429d94811d151c60a07eadf29b32847795f96c851e69a5fbd42543ca9173c2988335ef3c80a415ab4bcb5acb0458b4c49ae409d16c3e0d74c7105
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
class UserMailer < Pay.parent_mailer.constantize
|
|
3
3
|
def receipt
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
pay_charge = params[:pay_charge]
|
|
5
|
+
|
|
6
|
+
if pay_charge.respond_to? :receipt
|
|
7
|
+
attachments[pay_charge.receipt_filename] = pay_charge.receipt
|
|
8
|
+
attachments[pay_charge.invoice_filename] = pay_charge.invoice if Pay.attach_invoice_to_receipt
|
|
6
9
|
end
|
|
7
10
|
|
|
8
11
|
mail mail_arguments
|
|
@@ -39,3 +42,5 @@ module Pay
|
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
end
|
|
45
|
+
|
|
46
|
+
ActiveSupport.run_load_hooks :pay_user_mailer, Pay::UserMailer
|
data/app/models/pay/charge.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Pay
|
|
2
4
|
class Charge < Pay::ApplicationRecord
|
|
3
5
|
# Associations
|
|
@@ -30,11 +32,16 @@ module Pay
|
|
|
30
32
|
|
|
31
33
|
# Helpers for payment processors
|
|
32
34
|
%w[braintree stripe paddle_billing paddle_classic lemon_squeezy fake_processor].each do |processor_name|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
# Everything is inlined into the generated source so the scope lambda captures no variables,
|
|
36
|
+
# which lets Rails make it Ractor-shareable, and the predicate is a plain method rather than a
|
|
37
|
+
# closure, which lets a non-main Ractor call it.
|
|
38
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
39
|
+
scope :#{processor_name}, -> { joins(:customer).where(pay_customers: {processor: "#{processor_name}"}) }
|
|
40
|
+
|
|
41
|
+
def #{processor_name}?
|
|
42
|
+
customer.processor == "#{processor_name}"
|
|
43
|
+
end
|
|
44
|
+
RUBY
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
def self.find_by_processor_and_id(processor, processor_id)
|
data/app/models/pay/customer.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Pay
|
|
2
4
|
class Customer < Pay::ApplicationRecord
|
|
3
5
|
belongs_to :owner, polymorphic: true
|
|
@@ -21,11 +23,16 @@ module Pay
|
|
|
21
23
|
delegate :email, to: :owner, allow_nil: true
|
|
22
24
|
|
|
23
25
|
%w[stripe braintree paddle_billing paddle_classic lemon_squeezy fake_processor].each do |processor_name|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
# Everything is inlined into the generated source so the scope lambda captures no variables,
|
|
27
|
+
# which lets Rails make it Ractor-shareable, and the predicate is a plain method rather than a
|
|
28
|
+
# closure, which lets a non-main Ractor call it.
|
|
29
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
30
|
+
scope :#{processor_name}, -> { where(processor: "#{processor_name}") }
|
|
31
|
+
|
|
32
|
+
def #{processor_name}?
|
|
33
|
+
processor == "#{processor_name}"
|
|
34
|
+
end
|
|
35
|
+
RUBY
|
|
29
36
|
end
|
|
30
37
|
|
|
31
38
|
def update_payment_method(payment_method_id)
|
|
@@ -4,7 +4,7 @@ module Pay
|
|
|
4
4
|
extend Pay::Sync
|
|
5
5
|
|
|
6
6
|
# Lemon Squeezy statuses that Pay spells differently
|
|
7
|
-
STATUSES = {"on_trial" => "trialing", "cancelled" => "canceled"}
|
|
7
|
+
STATUSES = Pay.make_shareable({"on_trial" => "trialing", "cancelled" => "canceled"})
|
|
8
8
|
|
|
9
9
|
def self.sync(subscription_id, object: nil, name: Pay.default_product_name)
|
|
10
10
|
sync_with_retries do
|
|
@@ -3,7 +3,7 @@ module Pay
|
|
|
3
3
|
class Charge < Pay::Charge
|
|
4
4
|
extend Pay::Sync
|
|
5
5
|
|
|
6
|
-
EXPAND = ["balance_transaction", "payment_intent", "refunds.data.balance_transaction"]
|
|
6
|
+
EXPAND = Pay.make_shareable(["balance_transaction", "payment_intent", "refunds.data.balance_transaction"])
|
|
7
7
|
|
|
8
8
|
delegate :amount_captured, :payment_intent, to: :stripe_object, allow_nil: true
|
|
9
9
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Pay
|
|
2
4
|
class Subscription < Pay::ApplicationRecord
|
|
3
5
|
# Associations
|
|
@@ -34,11 +36,16 @@ module Pay
|
|
|
34
36
|
|
|
35
37
|
# Helper methods for payment processors
|
|
36
38
|
%w[braintree stripe paddle_billing paddle_classic lemon_squeezy fake_processor].each do |processor_name|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
# Everything is inlined into the generated source so the scope lambda captures no variables,
|
|
40
|
+
# which lets Rails make it Ractor-shareable, and the predicate is a plain method rather than a
|
|
41
|
+
# closure, which lets a non-main Ractor call it.
|
|
42
|
+
class_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
43
|
+
scope :#{processor_name}, -> { joins(:customer).where(pay_customers: {processor: "#{processor_name}"}) }
|
|
44
|
+
|
|
45
|
+
def #{processor_name}?
|
|
46
|
+
customer.processor == "#{processor_name}"
|
|
47
|
+
end
|
|
48
|
+
RUBY
|
|
42
49
|
end
|
|
43
50
|
|
|
44
51
|
delegate :owner, to: :customer
|
data/lib/pay/attributes.rb
CHANGED
|
@@ -8,9 +8,7 @@ module Pay
|
|
|
8
8
|
extend ActiveSupport::Concern
|
|
9
9
|
|
|
10
10
|
included do
|
|
11
|
-
|
|
12
|
-
cattr_accessor :pay_stripe_customer_attributes
|
|
13
|
-
cattr_accessor :pay_braintree_customer_attributes
|
|
11
|
+
class_attribute :pay_default_payment_processor, :pay_stripe_customer_attributes, :pay_braintree_customer_attributes
|
|
14
12
|
|
|
15
13
|
has_many :pay_customers, class_name: "Pay::Customer", as: :owner, inverse_of: :owner
|
|
16
14
|
has_many :pay_charges, through: :pay_customers, class_name: "Pay::Charge", source: :charges
|
data/lib/pay/braintree.rb
CHANGED
|
@@ -38,6 +38,8 @@ module Pay
|
|
|
38
38
|
|
|
39
39
|
extend Env
|
|
40
40
|
|
|
41
|
+
credential :public_key, :private_key, :merchant_id, :environment
|
|
42
|
+
|
|
41
43
|
def self.enabled?
|
|
42
44
|
return false unless Pay.enabled_processors.include?(:braintree) && defined?(::Braintree)
|
|
43
45
|
|
|
@@ -53,20 +55,8 @@ module Pay
|
|
|
53
55
|
)
|
|
54
56
|
end
|
|
55
57
|
|
|
56
|
-
def self.public_key
|
|
57
|
-
find_value_by_name(:braintree, :public_key)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def self.private_key
|
|
61
|
-
find_value_by_name(:braintree, :private_key)
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def self.merchant_id
|
|
65
|
-
find_value_by_name(:braintree, :merchant_id)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
58
|
def self.environment
|
|
69
|
-
|
|
59
|
+
super || "sandbox"
|
|
70
60
|
end
|
|
71
61
|
|
|
72
62
|
def self.configure_webhooks
|
data/lib/pay/engine.rb
CHANGED
|
@@ -4,11 +4,13 @@ module Pay
|
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
engine_name "pay"
|
|
6
6
|
|
|
7
|
+
# Rails main runs this from Rails::Application#ractorize! before it shares the application with Ractors
|
|
8
|
+
ActiveSupport.on_load(:before_ractorize) { Pay.make_shareable! }
|
|
9
|
+
|
|
7
10
|
initializer "pay.processors" do |app|
|
|
8
11
|
if Pay.automount_routes
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
end
|
|
12
|
+
# Rails instance_execs this on the route mapper, so it loses its self here: an engine instance can't be shared with Ractors
|
|
13
|
+
app.routes.append(&Pay.shareable_callable(proc { mount Pay::Engine, at: Pay.routes_path, as: "pay" }))
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
# Include the pay attributes for ActiveRecord models
|
|
@@ -21,7 +23,7 @@ module Pay
|
|
|
21
23
|
app.deprecators[:pay] = Pay.deprecator if app.respond_to?(:deprecators)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
RECEIPTS_VERSION = [">= 2", "< 4"]
|
|
26
|
+
RECEIPTS_VERSION = [">= 2", "< 4"].freeze
|
|
25
27
|
|
|
26
28
|
initializer "pay.receipts" do
|
|
27
29
|
if defined?(::Receipts::VERSION)
|
|
@@ -56,7 +58,7 @@ module Pay
|
|
|
56
58
|
|
|
57
59
|
# Determines if a gem version matches requirements
|
|
58
60
|
# Used for verifying that dependencies are correct
|
|
59
|
-
def version_matches?(current:, required:)
|
|
61
|
+
def self.version_matches?(current:, required:)
|
|
60
62
|
Gem::Dependency.new("gem", required).match? "gem", current
|
|
61
63
|
end
|
|
62
64
|
end
|
data/lib/pay/env.rb
CHANGED
|
@@ -1,27 +1,45 @@
|
|
|
1
1
|
module Pay
|
|
2
2
|
module Env
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# Search for environment variables
|
|
6
|
-
#
|
|
7
|
-
# We must handle a lot of different cases, including the new Rails 6
|
|
8
|
-
# environment separated credentials files which have no nesting for
|
|
9
|
-
# the current environment.
|
|
10
|
-
#
|
|
11
|
-
# 1. Check environment variable
|
|
12
|
-
# 2. Check environment scoped credentials
|
|
13
|
-
# 3. Check unscoped credentials
|
|
14
|
-
# 4. Check scoped and unscoped secrets (removed in Rails 7.2)
|
|
15
|
-
#
|
|
16
|
-
# For example, find_value_by_name("stripe", "private_key") will check the following in order until it finds a value:
|
|
3
|
+
# Declares the credentials a processor reads. Each gets a reader that looks the value up in this order:
|
|
17
4
|
#
|
|
18
5
|
# ENV["STRIPE_PRIVATE_KEY"]
|
|
19
6
|
# Rails.application.credentials.dig(:production, :stripe, :private_key)
|
|
20
7
|
# Rails.application.credentials.dig(:stripe, :private_key)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
8
|
+
#
|
|
9
|
+
# The readers live in an extended module, so a processor refines one with super (for a default, say).
|
|
10
|
+
# They are plain methods, not closures, so a Ractor can call them.
|
|
11
|
+
def credential(*names)
|
|
12
|
+
credential_names.concat(names)
|
|
13
|
+
|
|
14
|
+
readers = Module.new
|
|
15
|
+
names.each do |name|
|
|
16
|
+
readers.module_eval <<~RUBY, __FILE__, __LINE__ + 1
|
|
17
|
+
def #{name}
|
|
18
|
+
find_value_by_name(:#{name})
|
|
19
|
+
end
|
|
20
|
+
RUBY
|
|
21
|
+
end
|
|
22
|
+
extend readers
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Resolves every declared credential once and serves the frozen values from then on, so a Ractor never has to
|
|
26
|
+
# read ENV or the Rails credentials. Pay.make_shareable! calls this before Rails shares the application.
|
|
27
|
+
def freeze_credentials
|
|
28
|
+
@frozen_credentials = Pay.make_shareable(credential_names.to_h { |name| [name, find_value_by_name(name)] })
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def credential_names
|
|
34
|
+
@credential_names ||= []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_value_by_name(name)
|
|
38
|
+
return @frozen_credentials.fetch(name) if @frozen_credentials
|
|
39
|
+
|
|
40
|
+
ENV["#{credential_scope.upcase}_#{name.upcase}"] ||
|
|
41
|
+
credentials&.dig(env, credential_scope, name) ||
|
|
42
|
+
credentials&.dig(credential_scope, name)
|
|
25
43
|
rescue ActiveSupport::MessageEncryptor::InvalidMessage
|
|
26
44
|
Rails.logger.error <<~MESSAGE
|
|
27
45
|
Rails was unable to decrypt credentials. Pay checks the Rails credentials to look for API keys for payment processors.
|
|
@@ -32,6 +50,11 @@ module Pay
|
|
|
32
50
|
MESSAGE
|
|
33
51
|
end
|
|
34
52
|
|
|
53
|
+
# Pay::PaddleBilling reads :paddle_billing credentials
|
|
54
|
+
def credential_scope
|
|
55
|
+
@credential_scope ||= name.demodulize.underscore.to_sym
|
|
56
|
+
end
|
|
57
|
+
|
|
35
58
|
def env
|
|
36
59
|
Rails.env.to_sym
|
|
37
60
|
end
|
data/lib/pay/lemon_squeezy.rb
CHANGED
|
@@ -15,6 +15,8 @@ module Pay
|
|
|
15
15
|
|
|
16
16
|
extend Env
|
|
17
17
|
|
|
18
|
+
credential :api_key, :store_id, :signing_secret
|
|
19
|
+
|
|
18
20
|
def self.enabled?
|
|
19
21
|
return false unless Pay.enabled_processors.include?(:lemon_squeezy) && defined?(::LemonSqueezy)
|
|
20
22
|
|
|
@@ -25,18 +27,6 @@ module Pay
|
|
|
25
27
|
::LemonSqueezy.config.api_key = api_key
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
def self.api_key
|
|
29
|
-
find_value_by_name(:lemon_squeezy, :api_key)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.store_id
|
|
33
|
-
find_value_by_name(:lemon_squeezy, :store_id)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def self.signing_secret
|
|
37
|
-
find_value_by_name(:lemon_squeezy, :signing_secret)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
30
|
def self.passthrough(owner:, **options)
|
|
41
31
|
owner.to_sgid.to_s
|
|
42
32
|
end
|
data/lib/pay/paddle_billing.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Pay
|
|
2
4
|
module PaddleBilling
|
|
3
5
|
class Error < Pay::Error
|
|
@@ -14,6 +16,8 @@ module Pay
|
|
|
14
16
|
|
|
15
17
|
extend Env
|
|
16
18
|
|
|
19
|
+
credential :environment, :client_token, :api_key, :signing_secret
|
|
20
|
+
|
|
17
21
|
REQUIRED_VERSION = "~> 3"
|
|
18
22
|
|
|
19
23
|
def self.enabled?
|
|
@@ -29,19 +33,7 @@ module Pay
|
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def self.environment
|
|
32
|
-
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.client_token
|
|
36
|
-
find_value_by_name(:paddle_billing, :client_token)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def self.api_key
|
|
40
|
-
find_value_by_name(:paddle_billing, :api_key)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def self.signing_secret
|
|
44
|
-
find_value_by_name(:paddle_billing, :signing_secret)
|
|
36
|
+
super || "production"
|
|
45
37
|
end
|
|
46
38
|
|
|
47
39
|
def self.configure_webhooks
|
data/lib/pay/paddle_classic.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Pay
|
|
2
4
|
module PaddleClassic
|
|
3
5
|
class Error < Pay::Error
|
|
@@ -18,6 +20,8 @@ module Pay
|
|
|
18
20
|
|
|
19
21
|
extend Env
|
|
20
22
|
|
|
23
|
+
credential :vendor_id, :vendor_auth_code, :environment, :public_key, :public_key_file, :public_key_base64
|
|
24
|
+
|
|
21
25
|
REQUIRED_VERSION = "~> 3"
|
|
22
26
|
|
|
23
27
|
def self.enabled?
|
|
@@ -27,36 +31,25 @@ module Pay
|
|
|
27
31
|
current: ::Paddle::VERSION) || (raise "[Pay] paddle gem must be version #{REQUIRED_VERSION}")
|
|
28
32
|
end
|
|
29
33
|
|
|
34
|
+
# The client memoizes a Faraday connection, which can't be shared, so each non-main Ractor builds its own
|
|
30
35
|
def self.client
|
|
31
|
-
|
|
36
|
+
if ::Ractor.current == ::Ractor.main
|
|
37
|
+
@client ||= new_client
|
|
38
|
+
else
|
|
39
|
+
::Ractor.current[:pay_paddle_classic_client] ||= new_client
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.new_client
|
|
44
|
+
Paddle::Classic::Client.new(
|
|
32
45
|
vendor_id: vendor_id,
|
|
33
46
|
vendor_auth_code: vendor_auth_code,
|
|
34
47
|
sandbox: environment != "production"
|
|
35
48
|
)
|
|
36
49
|
end
|
|
37
50
|
|
|
38
|
-
def self.vendor_id
|
|
39
|
-
find_value_by_name(:paddle_classic, :vendor_id)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.vendor_auth_code
|
|
43
|
-
find_value_by_name(:paddle_classic, :vendor_auth_code)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
51
|
def self.environment
|
|
47
|
-
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def self.public_key
|
|
51
|
-
find_value_by_name(:paddle_classic, :public_key)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def self.public_key_file
|
|
55
|
-
find_value_by_name(:paddle_classic, :public_key_file)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def self.public_key_base64
|
|
59
|
-
find_value_by_name(:paddle_classic, :public_key_base64)
|
|
52
|
+
super || "production"
|
|
60
53
|
end
|
|
61
54
|
|
|
62
55
|
def self.passthrough(owner:, **options)
|
data/lib/pay/stripe.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Pay
|
|
2
4
|
module Stripe
|
|
3
5
|
class Error < Pay::Error
|
|
@@ -28,11 +30,18 @@ module Pay
|
|
|
28
30
|
|
|
29
31
|
extend Env
|
|
30
32
|
|
|
33
|
+
# context is optional: the account (acct_...) that API requests act on when private_key is a Stripe
|
|
34
|
+
# Organizations API key. https://docs.stripe.com/keys/organization-api-keys
|
|
35
|
+
credential :public_key, :private_key, :context, :signing_secret, :webhook_receive_test_events
|
|
36
|
+
|
|
31
37
|
REQUIRED_VERSION = "~> 20"
|
|
32
38
|
|
|
33
39
|
# A list of database model names that include Pay
|
|
34
40
|
# Used for safely looking up models with client_reference_id
|
|
35
|
-
|
|
41
|
+
class << self
|
|
42
|
+
attr_accessor :model_names
|
|
43
|
+
end
|
|
44
|
+
self.model_names = Set.new
|
|
36
45
|
|
|
37
46
|
def self.enabled?
|
|
38
47
|
return false unless Pay.enabled_processors.include?(:stripe) && defined?(::Stripe)
|
|
@@ -61,28 +70,8 @@ module Pay
|
|
|
61
70
|
::Stripe.max_network_retries = 2
|
|
62
71
|
end
|
|
63
72
|
|
|
64
|
-
def self.public_key
|
|
65
|
-
find_value_by_name(:stripe, :public_key)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def self.private_key
|
|
69
|
-
find_value_by_name(:stripe, :private_key)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Optional. The account (acct_...) that API requests should act on when
|
|
73
|
-
# using a Stripe Organizations API key. Resolved like every other Stripe
|
|
74
|
-
# credential: ENV["STRIPE_CONTEXT"], then env-scoped, then unscoped
|
|
75
|
-
# Rails credentials.
|
|
76
|
-
def self.context
|
|
77
|
-
find_value_by_name(:stripe, :context)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def self.signing_secret
|
|
81
|
-
find_value_by_name(:stripe, :signing_secret)
|
|
82
|
-
end
|
|
83
|
-
|
|
84
73
|
def self.webhook_receive_test_events
|
|
85
|
-
value =
|
|
74
|
+
value = super
|
|
86
75
|
value.blank? || ActiveModel::Type::Boolean.new.cast(value)
|
|
87
76
|
end
|
|
88
77
|
|
data/lib/pay/version.rb
CHANGED
data/lib/pay.rb
CHANGED
|
@@ -29,71 +29,67 @@ module Pay
|
|
|
29
29
|
autoload :SyncCustomer, "pay/billable/sync_customer"
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
mattr_accessor
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
# Configuration is kept in instance variables on this module rather than in class variables (mattr_accessor),
|
|
33
|
+
# because Ruby forbids reading class variables from a non-main Ractor. Pay.make_shareable! deep-freezes these
|
|
34
|
+
# values before Rails shares the application with Ractors.
|
|
35
|
+
class << self
|
|
36
|
+
attr_accessor :model_parent_class
|
|
37
|
+
|
|
38
|
+
# Business details for receipts
|
|
39
|
+
attr_accessor :application_name, :business_address, :business_name, :business_logo
|
|
40
|
+
attr_reader :support_email
|
|
41
|
+
|
|
42
|
+
attr_accessor :automount_routes, :routes_path
|
|
43
|
+
attr_accessor :default_product_name, :default_plan_name
|
|
44
|
+
attr_accessor :enabled_processors
|
|
45
|
+
attr_accessor :send_emails, :emails, :attach_invoice_to_receipt
|
|
46
|
+
attr_writer :mailer
|
|
47
|
+
attr_accessor :braintree_gateway
|
|
48
|
+
attr_accessor :parent_mailer, :mail_arguments, :mail_to
|
|
49
|
+
end
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
mattr_accessor :application_name
|
|
39
|
-
mattr_accessor :business_address
|
|
40
|
-
mattr_accessor :business_name
|
|
41
|
-
mattr_accessor :business_logo
|
|
42
|
-
mattr_reader :support_email
|
|
51
|
+
self.model_parent_class = "ApplicationRecord"
|
|
43
52
|
|
|
44
53
|
def self.support_email=(value)
|
|
45
|
-
|
|
54
|
+
address = value.is_a?(::Mail::Address) ? value : ::Mail::Address.new(value)
|
|
55
|
+
@support_email = address.tap(&:display_name) # memoized now, so the address still works once frozen
|
|
46
56
|
end
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
mattr_accessor :enabled_processors
|
|
61
|
-
@@enabled_processors = [:stripe, :braintree, :paddle_billing, :paddle_classic, :lemon_squeezy]
|
|
62
|
-
|
|
63
|
-
mattr_accessor :send_emails
|
|
64
|
-
@@send_emails = true
|
|
65
|
-
|
|
66
|
-
mattr_accessor :emails
|
|
67
|
-
@@emails = ActiveSupport::OrderedOptions.new
|
|
68
|
-
@@emails.payment_action_required = true
|
|
69
|
-
@@emails.payment_failed = true
|
|
70
|
-
@@emails.receipt = true
|
|
71
|
-
@@emails.refund = true
|
|
58
|
+
self.automount_routes = true
|
|
59
|
+
self.default_product_name = "default"
|
|
60
|
+
self.default_plan_name = "default"
|
|
61
|
+
self.routes_path = "/pay"
|
|
62
|
+
self.enabled_processors = [:stripe, :braintree, :paddle_billing, :paddle_classic, :lemon_squeezy]
|
|
63
|
+
self.send_emails = true
|
|
64
|
+
|
|
65
|
+
self.emails = ActiveSupport::OrderedOptions.new
|
|
66
|
+
emails.payment_action_required = true
|
|
67
|
+
emails.payment_failed = true
|
|
68
|
+
emails.receipt = true
|
|
69
|
+
emails.refund = true
|
|
72
70
|
# This only applies to Stripe, therefor we supply the second argument of price
|
|
73
|
-
|
|
71
|
+
emails.subscription_renewing = ->(pay_subscription, price) {
|
|
74
72
|
(price&.type == "recurring") && (price.recurring&.interval == "year")
|
|
75
73
|
}
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
emails.subscription_trial_will_end = true
|
|
75
|
+
emails.subscription_trial_ended = true
|
|
76
|
+
self.attach_invoice_to_receipt = true
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
@@mailer = "Pay::UserMailer"
|
|
78
|
+
self.mailer = "Pay::UserMailer"
|
|
81
79
|
|
|
82
80
|
# Resolved on every call rather than memoized, so code reloading in development returns the current class
|
|
83
81
|
def self.mailer
|
|
84
|
-
|
|
82
|
+
@mailer.constantize
|
|
85
83
|
end
|
|
86
84
|
|
|
87
85
|
def self.deprecator
|
|
88
86
|
@deprecator ||= ActiveSupport::Deprecation.new("13.0", "Pay")
|
|
89
87
|
end
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
@@parent_mailer = "Pay::ApplicationMailer"
|
|
89
|
+
self.parent_mailer = "Pay::ApplicationMailer"
|
|
93
90
|
|
|
94
91
|
# Should return a hash of arguments for the `mail` call in UserMailer
|
|
95
|
-
|
|
96
|
-
@@mail_arguments = -> {
|
|
92
|
+
self.mail_arguments = -> {
|
|
97
93
|
{
|
|
98
94
|
to: instance_exec(&Pay.mail_to),
|
|
99
95
|
subject: default_i18n_subject(application: Pay.application_name)
|
|
@@ -101,8 +97,7 @@ module Pay
|
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
# Should return String or Array of email recipients
|
|
104
|
-
|
|
105
|
-
@@mail_to = -> {
|
|
100
|
+
self.mail_to = -> {
|
|
106
101
|
if ::ActionMailer::Base.respond_to?(:email_address_with_name)
|
|
107
102
|
::ActionMailer::Base.email_address_with_name(params[:pay_customer].email, params[:pay_customer].customer_name)
|
|
108
103
|
else
|
|
@@ -134,13 +129,71 @@ module Pay
|
|
|
134
129
|
end
|
|
135
130
|
end
|
|
136
131
|
|
|
137
|
-
|
|
132
|
+
# Deep-freezes an object so every Ractor can read it
|
|
133
|
+
def self.make_shareable(object)
|
|
134
|
+
::Ractor.make_shareable(object)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Ruby 4.0 can re-create a proc with a different self. Earlier Rubies have Ractor without that.
|
|
138
|
+
def self.shareable_procs?
|
|
139
|
+
::Ractor.respond_to?(:shareable_proc)
|
|
140
|
+
end
|
|
141
|
+
private_class_method :shareable_procs?
|
|
142
|
+
|
|
143
|
+
# A proc written in an initializer has the initializer's `main` object as self, which can't be shared, so it is
|
|
144
|
+
# re-created with nil as self. Procs Pay runs with instance_exec get their self replaced anyway. Anything that
|
|
145
|
+
# isn't a proc, or any proc on a Ruby that can't do this, is returned as it is.
|
|
146
|
+
def self.shareable_callable(callable)
|
|
147
|
+
return callable unless shareable_procs? && callable.is_a?(Proc)
|
|
148
|
+
|
|
149
|
+
callable.lambda? ? ::Ractor.shareable_lambda(self: nil, &callable) : ::Ractor.shareable_proc(self: nil, &callable)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Prepares Pay for Rails' experimental Rails.application.ractorize!, which Rails runs from its before_ractorize
|
|
153
|
+
# load hook (see Pay::Engine). Configuration and credentials can't change afterwards.
|
|
154
|
+
def self.make_shareable!
|
|
155
|
+
return unless shareable_procs?
|
|
156
|
+
|
|
157
|
+
share_callable_options
|
|
158
|
+
share_customer_model_options
|
|
159
|
+
[Stripe, Braintree, PaddleBilling, PaddleClassic, LemonSqueezy].each(&:freeze_credentials)
|
|
160
|
+
make_shareable shareable_configuration
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.share_callable_options
|
|
164
|
+
self.send_emails = shareable_callable(send_emails)
|
|
165
|
+
self.mail_to = shareable_callable(mail_to)
|
|
166
|
+
self.mail_arguments = shareable_callable(mail_arguments)
|
|
167
|
+
emails.transform_values! { |option| shareable_callable(option) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def self.share_customer_model_options
|
|
171
|
+
customer_models = ActiveRecord::Base.descendants.select { |model| model.include?(Pay::Attributes::CustomerExtension) }
|
|
172
|
+
customer_models.each do |model|
|
|
173
|
+
model.pay_stripe_customer_attributes = shareable_callable(model.pay_stripe_customer_attributes)
|
|
174
|
+
model.pay_braintree_customer_attributes = shareable_callable(model.pay_braintree_customer_attributes)
|
|
175
|
+
make_shareable [model.pay_default_payment_processor, model.pay_stripe_customer_attributes, model.pay_braintree_customer_attributes]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Everything a Ractor may read
|
|
180
|
+
def self.shareable_configuration
|
|
181
|
+
[
|
|
182
|
+
model_parent_class, application_name, business_address, business_name, business_logo, support_email,
|
|
183
|
+
automount_routes, routes_path, default_product_name, default_plan_name, enabled_processors,
|
|
184
|
+
send_emails, emails, attach_invoice_to_receipt, @mailer, parent_mailer, mail_arguments, mail_to,
|
|
185
|
+
Stripe.model_names, Currency.all, Webhooks.delegator, deprecator
|
|
186
|
+
]
|
|
187
|
+
end
|
|
188
|
+
private_class_method :share_callable_options, :share_customer_model_options, :shareable_configuration
|
|
189
|
+
|
|
190
|
+
SYNC_HANDLERS = make_shareable({
|
|
138
191
|
lemon_squeezy_order_id: ->(param) { Pay::LemonSqueezy.sync_order(param) },
|
|
139
192
|
paddle_billing_transaction_id: ->(param) { Pay::PaddleBilling.sync_transaction(param) },
|
|
140
193
|
stripe_checkout_session_id: ->(param) { Pay::Stripe.sync_checkout_session(param) },
|
|
141
194
|
transaction_id: ->(param) { Pay::PaddleBilling.sync_transaction(param) },
|
|
142
195
|
session_id: ->(param) { Pay::Stripe.sync_checkout_session(param) }
|
|
143
|
-
}
|
|
196
|
+
})
|
|
144
197
|
|
|
145
198
|
def self.sync(params)
|
|
146
199
|
SYNC_HANDLERS.each do |param_name, handler|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pay
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 12.
|
|
4
|
+
version: 12.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jason Charnes
|
|
@@ -184,7 +184,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
184
184
|
requirements:
|
|
185
185
|
- - ">="
|
|
186
186
|
- !ruby/object:Gem::Version
|
|
187
|
-
version: '0'
|
|
187
|
+
version: '3.0'
|
|
188
188
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
189
189
|
requirements:
|
|
190
190
|
- - ">="
|