pay 11.7.2 → 11.8.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: 3c2ba6d976f257c274ab9d25a84e91e13a8917be7b39100f47e9b33430cee814
4
- data.tar.gz: 6a44d98ade15ef41f4a12764db46798f33840d027198db190ae49ef32264fcd8
3
+ metadata.gz: 0d074d1f459f7129fb3f7e308075e87054d667f177453a89a08f3b243a4a029a
4
+ data.tar.gz: b6d3340eb343e55a964f925357076b76f751a2e1bae87fa262221682869cb9a7
5
5
  SHA512:
6
- metadata.gz: 57ff1cc4b2ff4400f6146dc9c0957254fcb918008f48394d28145eff032db91a3a308471f404beaa4512418b911405f7b553061b59ff7649b9bb07285e167558
7
- data.tar.gz: 222dd985ce44ab7218077e599670a55b35d4dd8ad0ed77efa907a731e382ec0c4f7931e9f82f7dd9650395e70df927b29da5c8e75b4c9825723be5ebd435c5d8
6
+ metadata.gz: c5b8ad8fad63c66e64d5c4f03f06dd52b06e981342a1388a8b2ec405dd8438f3ba7cbc1256db54658d8e051e84370fb91806a8b3b916674d532c5ed048d5791f
7
+ data.tar.gz: cb8f4b0b6ec9450846a947876ac11da6b44d7bab9c595253179df16f50244d76413d491f2fc4ba25248b6101112ba5ba9ef406e1e95c8b6b418c792ed157442e
data/README.md CHANGED
@@ -50,6 +50,7 @@ Want to add a new payment provider? Contributions are welcome.
50
50
  * [Lemon Squeezy](docs/lemon_squeezy/1_overview.md)
51
51
  * [Fake Processor](docs/fake_processor/1_overview.md)
52
52
  * [Asaas (Community)](https://github.com/PedroAugustoRamalhoDuarte/pay-asaas)
53
+ * [Square (Community)](https://github.com/Willardgmoore/pay-square)
53
54
  * **Marketplaces**
54
55
  * [Stripe Connect](docs/marketplaces/stripe_connect.md)
55
56
  * **Contributing**
@@ -4,8 +4,9 @@ module Pay
4
4
 
5
5
  before_action :set_redirect_to
6
6
 
7
+ # Intents on a Stripe Connect account are linked with ?stripe_account=acct_123
7
8
  def show
8
- @payment = Payment.from_id(params[:id])
9
+ @payment = Payment.from_id(params[:id], stripe_account: params[:stripe_account].presence)
9
10
  rescue ::Stripe::StripeError => e
10
11
  redirect_to root_path, alert: e.message
11
12
  end
@@ -27,6 +27,9 @@ module Pay
27
27
  return
28
28
  end
29
29
 
30
+ # Requests for the rest of the sync should go to the same Stripe Connect account as the customer
31
+ stripe_account ||= pay_customer.stripe_account
32
+
30
33
  payment_method = object.payment_method_details.try(object.payment_method_details.type)
31
34
  attrs = {
32
35
  object: object.to_hash,
@@ -42,7 +45,7 @@ module Pay
42
45
  last4: payment_method.try(:last4).to_s,
43
46
  metadata: object.metadata,
44
47
  payment_method_type: object.payment_method_details.type,
45
- stripe_account: pay_customer.stripe_account,
48
+ stripe_account: stripe_account,
46
49
  stripe_receipt_url: object.receipt_url
47
50
  }
48
51
 
@@ -72,7 +75,7 @@ module Pay
72
75
  raise
73
76
  else
74
77
  try += 1
75
- sleep 0.15**try
78
+ sleep 0.15 * try
76
79
  retry
77
80
  end
78
81
  end
@@ -124,7 +127,7 @@ module Pay
124
127
  raise Pay::Stripe::Error, "no payment_intent on charge" unless payment_intent.present?
125
128
  payment_intent_id = payment_intent.is_a?(::Stripe::PaymentIntent) ? payment_intent.id : payment_intent
126
129
  ::Stripe::PaymentIntent.capture(payment_intent_id, options, stripe_options)
127
- self.class.sync(processor_id)
130
+ self.class.sync(processor_id, stripe_account: stripe_account)
128
131
  rescue ::Stripe::StripeError => e
129
132
  raise Pay::Stripe::Error, e
130
133
  end
@@ -144,6 +147,10 @@ module Pay
144
147
  ::Stripe::Charge.construct_from(object)
145
148
  end
146
149
 
150
+ def sync!(**options)
151
+ super(**options.with_defaults(stripe_account: stripe_account))
152
+ end
153
+
147
154
  private
148
155
 
149
156
  # Options for Stripe requests
@@ -50,7 +50,7 @@ module Pay
50
50
  Pay::Payment.new(payment_intent).validate
51
51
 
52
52
  charge = payment_intent.latest_charge
53
- Pay::Stripe::Charge.sync(charge.id, object: charge)
53
+ Pay::Stripe::Charge.sync(charge.id, object: charge, stripe_account: stripe_account)
54
54
  rescue ::Stripe::StripeError => e
55
55
  raise Pay::Stripe::Error, e
56
56
  end
@@ -73,7 +73,7 @@ module Pay
73
73
  # No trial, payment method requires SCA
74
74
  if options[:payment_behavior].to_s != "default_incomplete" && subscription.incomplete?
75
75
  payment_intent_id = stripe_sub.latest_invoice.payments.first.payment.payment_intent
76
- Pay::Payment.from_id(payment_intent_id).validate
76
+ Pay::Payment.from_id(payment_intent_id, stripe_account: stripe_account).validate
77
77
  end
78
78
 
79
79
  subscription
@@ -151,7 +151,7 @@ module Pay
151
151
  def sync_subscriptions(**options)
152
152
  subscriptions = ::Stripe::Subscription.list(options.with_defaults(customer: processor_id), stripe_options)
153
153
  subscriptions.map do |subscription|
154
- Pay::Stripe::Subscription.sync(subscription.id)
154
+ Pay::Stripe::Subscription.sync(subscription.id, stripe_account: stripe_account)
155
155
  end
156
156
  rescue ::Stripe::StripeError => e
157
157
  raise Pay::Stripe::Error, e
@@ -251,7 +251,7 @@ module Pay
251
251
  ::Stripe::Billing::MeterEvent.create({
252
252
  event_name: event_name,
253
253
  payload: {stripe_customer_id: processor_id}.merge(payload)
254
- }.merge(options))
254
+ }.merge(options), stripe_options)
255
255
  end
256
256
 
257
257
  private
@@ -28,7 +28,7 @@ module Pay
28
28
 
29
29
  # A single-use login link for Express accounts to access their Stripe dashboard
30
30
  def login_link(**options)
31
- ::Stripe::Account.create_login_link(processor_id)
31
+ ::Stripe::Account.create_login_link(processor_id, options)
32
32
  rescue ::Stripe::StripeError => e
33
33
  raise Pay::Stripe::Error, e
34
34
  end
@@ -6,7 +6,7 @@ module Pay
6
6
  payment_intent = ::Stripe::PaymentIntent.retrieve({id: id, expand: ["payment_method"]}, {stripe_account: stripe_account}.compact)
7
7
  payment_method = payment_intent.payment_method
8
8
  return unless payment_method
9
- Pay::Stripe::PaymentMethod.sync(payment_method.id, object: payment_method)
9
+ Pay::Stripe::PaymentMethod.sync(payment_method.id, object: payment_method, stripe_account: stripe_account)
10
10
  end
11
11
 
12
12
  # Syncs a SetupIntent's payment method to the database
@@ -14,7 +14,7 @@ module Pay
14
14
  setup_intent = ::Stripe::SetupIntent.retrieve({id: id, expand: ["payment_method"]}, {stripe_account: stripe_account}.compact)
15
15
  payment_method = setup_intent.payment_method
16
16
  return unless payment_method
17
- Pay::Stripe::PaymentMethod.sync(payment_method.id, object: payment_method)
17
+ Pay::Stripe::PaymentMethod.sync(payment_method.id, object: payment_method, stripe_account: stripe_account)
18
18
  end
19
19
 
20
20
  # Syncs PaymentMethod objects from Stripe
@@ -31,6 +31,9 @@ module Pay
31
31
  return
32
32
  end
33
33
 
34
+ # Record the same Stripe Connect account as the customer when one wasn't given
35
+ stripe_account ||= pay_customer.stripe_account
36
+
34
37
  default_payment_method_id = pay_customer.api_record.invoice_settings&.default_payment_method
35
38
  default = (id == default_payment_method_id)
36
39
 
@@ -45,7 +48,7 @@ module Pay
45
48
  raise
46
49
  else
47
50
  try += 1
48
- sleep 0.15**try
51
+ sleep 0.15 * try
49
52
  retry
50
53
  end
51
54
  end
@@ -5,7 +5,7 @@ module Pay
5
5
 
6
6
  def self.sync_from_checkout_session(session_id, stripe_account: nil)
7
7
  checkout_session = ::Stripe::Checkout::Session.retrieve({id: session_id}, {stripe_account: stripe_account}.compact)
8
- sync(checkout_session.subscription)
8
+ sync(checkout_session.subscription, stripe_account: stripe_account)
9
9
  end
10
10
 
11
11
  def self.sync(subscription_id, object: nil, name: nil, stripe_account: nil, try: 0, retries: 1)
@@ -22,6 +22,9 @@ module Pay
22
22
  return
23
23
  end
24
24
 
25
+ # Requests for the rest of the sync should go to the same Stripe Connect account as the customer
26
+ stripe_account ||= pay_customer.stripe_account
27
+
25
28
  attributes = {
26
29
  object: object.to_hash,
27
30
  application_fee_percent: object.application_fee_percent,
@@ -29,7 +32,7 @@ module Pay
29
32
  processor_plan: object.items.first.price.id,
30
33
  quantity: object.items.first.try(:quantity) || 0,
31
34
  status: object.status,
32
- stripe_account: pay_customer.stripe_account,
35
+ stripe_account: stripe_account,
33
36
  metadata: object.metadata,
34
37
  metered: false,
35
38
  pause_behavior: object.pause_collection&.behavior,
@@ -70,10 +73,10 @@ module Pay
70
73
  # Sync payment method if directly attached to subscription
71
74
  if object.default_payment_method
72
75
  if object.default_payment_method.is_a? String
73
- Pay::Stripe::PaymentMethod.sync(object.default_payment_method)
76
+ Pay::Stripe::PaymentMethod.sync(object.default_payment_method, stripe_account: stripe_account)
74
77
  attributes[:payment_method_id] = object.default_payment_method
75
78
  else
76
- Pay::Stripe::PaymentMethod.sync(object.default_payment_method.id, object: object.default_payment_method)
79
+ Pay::Stripe::PaymentMethod.sync(object.default_payment_method.id, object: object.default_payment_method, stripe_account: stripe_account)
77
80
  attributes[:payment_method_id] = object.default_payment_method.id
78
81
  end
79
82
  end
@@ -106,9 +109,9 @@ module Pay
106
109
 
107
110
  case invoice_payment.payment.type
108
111
  when "payment_intent"
109
- Pay::Stripe::Charge.sync_payment_intent(invoice_payment.payment.payment_intent, stripe_account: pay_subscription.stripe_account)
112
+ Pay::Stripe::Charge.sync_payment_intent(invoice_payment.payment.payment_intent, stripe_account: stripe_account)
110
113
  when "charge"
111
- Pay::Stripe::Charge.sync(invoice_payment.payment.charge, stripe_account: pay_subscription.stripe_account)
114
+ Pay::Stripe::Charge.sync(invoice_payment.payment.charge, stripe_account: stripe_account)
112
115
  end
113
116
  end
114
117
  end
@@ -144,6 +147,10 @@ module Pay
144
147
  ::Stripe::Subscription.construct_from(object)
145
148
  end
146
149
 
150
+ def sync!(**options)
151
+ super(**options.with_defaults(stripe_account: stripe_account))
152
+ end
153
+
147
154
  def api_record(**options)
148
155
  @api_record ||= ::Stripe::Subscription.retrieve(options.with_defaults(id: processor_id).merge(expand_options), {stripe_account: stripe_account}.compact)
149
156
  end
@@ -198,16 +205,12 @@ module Pay
198
205
 
199
206
  # This updates a SubscriptionItem's quantity in Stripe
200
207
  #
201
- # For a subscription with a single item, we can update the subscription directly if no SubscriptionItem ID is available
202
- # Otherwise a SubscriptionItem ID is required so Stripe knows which entry to update
208
+ # Defaults to the first SubscriptionItem. Pass subscription_item_id: to update a different one
209
+ # (Stripe no longer accepts a top-level quantity on the subscription itself)
203
210
  def change_quantity(quantity, **options)
204
- subscription_item_id = options.delete(:subscription_item_id) || subscription_items&.first&.id
205
- if subscription_item_id
206
- ::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
207
- @api_record = nil
208
- else
209
- @api_record = ::Stripe::Subscription.update(processor_id, options.merge(quantity: quantity).merge(expand_options), stripe_options)
210
- end
211
+ subscription_item_id = options.delete(:subscription_item_id) || subscription_items.first.id
212
+ ::Stripe::SubscriptionItem.update(subscription_item_id, options.merge(quantity: quantity), stripe_options)
213
+ @api_record = nil
211
214
  update(quantity: quantity)
212
215
  rescue ::Stripe::StripeError => e
213
216
  raise Pay::Stripe::Error, e
@@ -285,7 +288,7 @@ module Pay
285
288
  }.merge(expand_options),
286
289
  stripe_options)
287
290
  end
288
- update(ends_at: nil, status: :active)
291
+ update(ends_at: nil, status: @api_record.status)
289
292
  rescue ::Stripe::StripeError => e
290
293
  raise Pay::Stripe::Error, e
291
294
  end
@@ -309,7 +312,7 @@ module Pay
309
312
 
310
313
  # Validate that swap was successful and handle SCA if needed
311
314
  if (payment_intent_id = @api_record.latest_invoice.payments.first&.payment&.payment_intent)
312
- Pay::Payment.from_id(payment_intent_id).validate
315
+ Pay::Payment.from_id(payment_intent_id, stripe_account: stripe_account).validate
313
316
  end
314
317
 
315
318
  sync!(object: @api_record)
@@ -338,7 +341,10 @@ module Pay
338
341
  payment_intent = ::Stripe::PaymentIntent.retrieve({id: payment_intent_id}, stripe_options)
339
342
 
340
343
  payment_intent = if payment_intent.status == "requires_payment_method"
341
- ::Stripe::PaymentIntent.confirm(payment_intent_id, {payment_method: customer.default_payment_method.processor_id}, stripe_options)
344
+ payment_method = customer.default_payment_method
345
+ raise Pay::Stripe::Error, "no default payment method to retry the payment with" if payment_method.nil?
346
+
347
+ ::Stripe::PaymentIntent.confirm(payment_intent_id, {payment_method: payment_method.processor_id}, stripe_options)
342
348
  else
343
349
  ::Stripe::PaymentIntent.confirm(payment_intent_id, stripe_options)
344
350
  end
@@ -348,14 +354,21 @@ module Pay
348
354
  end
349
355
 
350
356
  # Looks up open invoices for a subscription and attempts to pay them
357
+ #
358
+ # Invoices no longer expose a `payment_intent` directly, so the PaymentIntent is looked up through the invoice's payments
351
359
  def pay_open_invoices
352
- ::Stripe::Invoice.list({subscription: processor_id, status: :open}, stripe_options).auto_paging_each do |invoice|
353
- retry_failed_payment(payment_intent_id: invoice.payment_intent)
360
+ ::Stripe::Invoice.list({subscription: processor_id, status: :open, expand: ["data.payments"]}, stripe_options).auto_paging_each do |invoice|
361
+ payment_intent_id = invoice.payments.first&.payment&.payment_intent
362
+ retry_failed_payment(payment_intent_id: payment_intent_id) if payment_intent_id
354
363
  end
355
364
  end
356
365
 
366
+ # Returns the Stripe::PaymentIntent for the latest invoice, if there is one
357
367
  def latest_payment
358
- api_record(expand: ["latest_invoice.payment_intent"]).latest_invoice.payment_intent
368
+ payment_intent_id = api_record.latest_invoice&.payments&.first&.payment&.payment_intent
369
+ ::Stripe::PaymentIntent.retrieve({id: payment_intent_id}, stripe_options) if payment_intent_id
370
+ rescue ::Stripe::StripeError => e
371
+ raise Pay::Stripe::Error, e
359
372
  end
360
373
 
361
374
  private
@@ -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 %>', <%= {stripeAccount: @payment.stripe_account}.compact.to_json.html_safe %>)
68
68
 
69
69
  import { Application, Controller } from 'https://unpkg.com/@hotwired/stimulus/dist/stimulus.js'
70
70
  const application = Application.start()
@@ -1,5 +1,5 @@
1
1
  <h3>Extra confirmation is needed to process your payment</h3>
2
2
  <p>Your <%= Pay.application_name %> subscription requires confirmation to process your payment to continue access.</p>
3
- <p><%= link_to "Confirm your payment", pay.payment_url(params[:payment_intent_id]) %></p>
3
+ <p><%= link_to "Confirm your payment", pay.payment_url(params[:payment_intent_id], stripe_account: params[:stripe_account]) %></p>
4
4
  <p>If you have any questions, please hit reply and let us know.</p>
5
5
  <p>— The <%= Pay.application_name %> Team</p>
@@ -2,7 +2,7 @@ Extra confirmation is needed to process your payment
2
2
 
3
3
  Your <%= Pay.application_name %> subscription requires confirmation to process your payment to continue access.
4
4
 
5
- Confirm your payment: <%= pay.payment_url(params[:payment_intent_id]) %>
5
+ Confirm your payment: <%= pay.payment_url(params[:payment_intent_id], stripe_account: params[:stripe_account]) %>
6
6
 
7
7
  If you have any questions, please hit reply and let us know.
8
8
 
data/lib/pay/payment.rb CHANGED
@@ -1,16 +1,20 @@
1
1
  module Pay
2
2
  class Payment
3
- attr_reader :intent
3
+ # The Stripe Connect account the intent lives on, if any. Needed to look the intent up again
4
+ # and to initialize Stripe.js on the SCA confirmation page.
5
+ attr_reader :intent, :stripe_account
4
6
 
5
7
  delegate :id, :amount, :client_secret, :currency, :customer, :status, :confirm, to: :intent
6
8
 
7
- def self.from_id(id)
8
- intent = id.start_with?("seti_") ? ::Stripe::SetupIntent.retrieve(id) : ::Stripe::PaymentIntent.retrieve(id)
9
- new(intent)
9
+ def self.from_id(id, stripe_account: nil)
10
+ options = {stripe_account: stripe_account}.compact
11
+ intent = id.start_with?("seti_") ? ::Stripe::SetupIntent.retrieve(id, options) : ::Stripe::PaymentIntent.retrieve(id, options)
12
+ new(intent, stripe_account: stripe_account)
10
13
  end
11
14
 
12
- def initialize(intent)
15
+ def initialize(intent, stripe_account: nil)
13
16
  @intent = intent
17
+ @stripe_account = stripe_account
14
18
  end
15
19
 
16
20
  def requires_payment_method?
@@ -3,7 +3,7 @@ module Pay
3
3
  module Webhooks
4
4
  class CheckoutSessionCompleted
5
5
  def call(event)
6
- locate_owner(event.data.object)
6
+ locate_owner(event.data.object, stripe_account: event.try(:account))
7
7
 
8
8
  # By the time CheckoutSessionCompleted is fired, we probably missed the original events
9
9
  # Instead, we can sync the payment intent or subscription during this event to ensure they're in the database
@@ -18,11 +18,15 @@ module Pay
18
18
  end
19
19
  end
20
20
 
21
- def locate_owner(object)
22
- return if object.client_reference_id.nil?
21
+ # Associates the Stripe customer from the session with the record referenced by client_reference_id
22
+ #
23
+ # Sessions in payment or setup mode don't always create a Stripe customer (for example Payment Links with
24
+ # customer_creation: if_required), so skip those rather than clearing an existing processor_id
25
+ def locate_owner(object, stripe_account: nil)
26
+ return if object.client_reference_id.nil? || object.customer.nil?
23
27
 
24
28
  owner = Pay::Stripe.find_by_client_reference_id(object.client_reference_id)
25
- owner&.add_payment_processor(:stripe, processor_id: object.customer)
29
+ owner&.add_payment_processor(:stripe, processor_id: object.customer, stripe_account: stripe_account)
26
30
  end
27
31
  end
28
32
  end
@@ -19,10 +19,13 @@ module Pay
19
19
  return unless subscription
20
20
 
21
21
  # Grab the local latest_invoice from the subscription stripe_object
22
+ # It may be an expanded object, a bare ID, or nil for a subscription that hasn't been invoiced yet
22
23
  latest_invoice = subscription.stripe_object.try(:latest_invoice)
24
+ latest_invoice_id = latest_invoice.try(:id) || latest_invoice
25
+ return if latest_invoice_id.blank?
23
26
 
24
27
  # Compare the local invoice id to the event invoice id and sync if they are the same
25
- if latest_invoice.id.to_s == invoice.id.to_s
28
+ if latest_invoice_id.to_s == invoice.id.to_s
26
29
  Pay::Stripe::Subscription.sync(subscription_id, stripe_account: event.try(:account))
27
30
  end
28
31
  rescue ::Stripe::StripeError => e
@@ -13,13 +13,14 @@ module Pay
13
13
  pay_subscription = Pay::Subscription.find_by_processor_and_id(:stripe, subscription_id)
14
14
  return if pay_subscription.nil? || pay_subscription.status == "incomplete"
15
15
 
16
- invoice_payment = ::Stripe::InvoicePayment.list({invoice: invoice.id, status: :open}).first
16
+ invoice_payment = ::Stripe::InvoicePayment.list({invoice: invoice.id, status: :open}, {stripe_account: event.try(:account)}.compact).first
17
17
 
18
18
  if invoice_payment && Pay.send_email?(:payment_action_required, pay_subscription)
19
19
  Pay.mailer.with(
20
20
  pay_customer: pay_subscription.customer,
21
21
  pay_subscription: pay_subscription,
22
- payment_intent_id: invoice_payment.payment.payment_intent
22
+ payment_intent_id: invoice_payment.payment.payment_intent,
23
+ stripe_account: event.try(:account)
23
24
  ).payment_action_required.deliver_later
24
25
  end
25
26
  end
data/lib/pay/stripe.rb CHANGED
@@ -180,7 +180,7 @@ module Pay
180
180
  raise
181
181
  else
182
182
  try += 1
183
- sleep 0.15**try
183
+ sleep 0.15 * try
184
184
  retry
185
185
  end
186
186
  end
data/lib/pay/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pay
2
- VERSION = "11.7.2"
2
+ VERSION = "11.8.0"
3
3
  end
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: 11.7.2
4
+ version: 11.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Charnes