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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f77372d0f6b47b272a614d14cd91183c86b56dd02d2ecea25fd99ebccde3ec7b
4
- data.tar.gz: 7c6c0b2196445b486b0519a14c1fe1a1123c81316822af0cc2fea3c891c731dd
3
+ metadata.gz: 40ac78c02d095d82b2639d4755d68795de86c38bf281e125663268521c295fea
4
+ data.tar.gz: 7252f8657c37679b590863bc28a901ef6547bfdae139555a9a4d4fd8eb523f90
5
5
  SHA512:
6
- metadata.gz: e30e10e229a3af4a2dea25d532c209831c9388222226d97b1c576e4ddf3aee7509da290a2fa6f762dd6a687bea07962e5c309c22ce1cca84a337d6edc979057c
7
- data.tar.gz: 4878549cc6eb89db69734ae9bbec6d2e9fa4d6027322af4e266e89c5a026ebb19c946a202b63327f55c3b3da8b9384112ace9310effe1c9552a4ede2efef0516
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
- if params[:pay_charge].respond_to? :receipt
5
- attachments[params[:pay_charge].receipt_filename] = params[:pay_charge].receipt
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
@@ -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
- define_method :"#{processor_name}?" do
34
- customer.processor == processor_name
35
- end
36
-
37
- scope processor_name, -> { joins(:customer).where(pay_customers: {processor: processor_name}) }
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)
@@ -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
- scope processor_name, -> { where(processor: processor_name) }
25
-
26
- define_method :"#{processor_name}?" do
27
- processor == processor_name
28
- end
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"}.freeze
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
- define_method :"#{processor_name}?" do
38
- customer.processor == processor_name
39
- end
40
-
41
- scope processor_name, -> { joins(:customer).where(pay_customers: {processor: processor_name}) }
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
@@ -8,9 +8,7 @@ module Pay
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  included do
11
- cattr_accessor :pay_default_payment_processor
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
- find_value_by_name(:braintree, :environment) || "sandbox"
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
- app.routes.append do
10
- mount Pay::Engine, at: Pay.routes_path, as: "pay"
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
- private
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
- def find_value_by_name(scope, name)
22
- ENV["#{scope.upcase}_#{name.upcase}"] ||
23
- credentials&.dig(env, scope, name) ||
24
- credentials&.dig(scope, name)
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
@@ -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
@@ -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
- find_value_by_name(:paddle_billing, :environment) || "production"
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
@@ -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
- @client ||= Paddle::Classic::Client.new(
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
- find_value_by_name(:paddle_classic, :environment) || "production"
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
- mattr_accessor :model_names, default: Set.new
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 = find_value_by_name(:stripe, :webhook_receive_test_events)
74
+ value = super
86
75
  value.blank? || ActiveModel::Type::Boolean.new.cast(value)
87
76
  end
88
77
 
data/lib/pay/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Pay
2
- VERSION = "12.3.0"
4
+ VERSION = "12.4.0"
3
5
  end
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 :braintree_gateway
33
-
34
- mattr_accessor :model_parent_class
35
- @@model_parent_class = "ApplicationRecord"
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
- # Business details for receipts
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
- @@support_email = value.is_a?(::Mail::Address) ? value : ::Mail::Address.new(value)
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
- mattr_accessor :automount_routes
49
- @@automount_routes = true
50
-
51
- mattr_accessor :default_product_name
52
- @@default_product_name = "default"
53
-
54
- mattr_accessor :default_plan_name
55
- @@default_plan_name = "default"
56
-
57
- mattr_accessor :routes_path
58
- @@routes_path = "/pay"
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
- @@emails.subscription_renewing = ->(pay_subscription, price) {
71
+ emails.subscription_renewing = ->(pay_subscription, price) {
74
72
  (price&.type == "recurring") && (price.recurring&.interval == "year")
75
73
  }
76
- @@emails.subscription_trial_will_end = true
77
- @@emails.subscription_trial_ended = true
74
+ emails.subscription_trial_will_end = true
75
+ emails.subscription_trial_ended = true
76
+ self.attach_invoice_to_receipt = true
78
77
 
79
- mattr_writer :mailer
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
- @@mailer.constantize
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
- mattr_accessor :parent_mailer
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
- mattr_accessor :mail_arguments
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
- mattr_accessor :mail_to
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
- SYNC_HANDLERS = {
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.3.0
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
  - - ">="