pay 12.1.1 → 12.2.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: 2cc4265d73925a1e3134b5dafcdf452b131b8c478c2d9f04179148d313ac3275
4
- data.tar.gz: 02f84e17b0216f30d5d38d92455adc13a9fd14fff96a84f5ee045041340dc999
3
+ metadata.gz: 19c2b44409eb406bf144f15e922e863e8265c0d65bba50141928da748ed0d0f0
4
+ data.tar.gz: c9dbd51994ab4747a672905bb3b6a0fadf9060ef46ebaaf5edd6acd3f4569197
5
5
  SHA512:
6
- metadata.gz: fdf6618be0a7510e7cf015ee9e80a0e08b1550953c739fb978adfa1f3b74fe6ad58244deef09bdc52284bf92dcc1013e14daaa70d49d5516c85b0f63bcbbd701
7
- data.tar.gz: be11d81512c0dc629be7eaf13070c6382fa1c30f3606bc05b0ce2c882a8b9b3bf5760b5e52be48efe2353e6a0773225b2cc473c1b5e194153e515a57e459f701
6
+ metadata.gz: b1ab458e90aaede5cdc5bb5418917c4084736bc4c48544b3d360ee4e0fac8975db1b24e363e218e2b2986837c7c97ea9724011b3196f00f9e85e9a7b0a26dce6
7
+ data.tar.gz: 744063befa0467573287d6264a87419018d63fd845a4cdd307f0cb0ecc08bb4570eb839524f9b225c3aeda0e7fdb1a1c095321e4090222c3f29d5355043e7d42
@@ -39,8 +39,6 @@ module Pay
39
39
  pay_customer.payment_methods.destroy_all
40
40
  when "trialing"
41
41
  attributes[:trial_ends_at] = Time.parse(subscription.next_billed_at) if subscription.next_billed_at
42
- when "paused"
43
- attributes[:pause_starts_at] = Time.parse(subscription.paused_at) if subscription.paused_at
44
42
  when "active", "past_due"
45
43
  attributes[:trial_ends_at] = nil
46
44
  attributes[:pause_starts_at] = nil
@@ -76,10 +74,12 @@ module Pay
76
74
  end
77
75
 
78
76
  # If a subscription is paused, cancel immediately
79
- # Otherwise, cancel at period end
77
+ # Otherwise, cancel at period end. A scheduled pause is removed first
80
78
  def cancel(**options)
81
79
  return if canceled?
82
80
 
81
+ remove_scheduled_pause if will_pause?
82
+
83
83
  response = ::Paddle::Subscription.cancel(
84
84
  id: processor_id,
85
85
  effective_from: options.fetch(:effective_from, paused? ? "immediately" : "next_billing_period")
@@ -94,8 +94,6 @@ module Pay
94
94
 
95
95
  def cancel_now!(**options)
96
96
  cancel(**options.merge(effective_from: "immediately"))
97
- rescue ::Paddle::Error => e
98
- raise Pay::PaddleBilling::Error, e
99
97
  end
100
98
 
101
99
  def change_quantity(quantity, **options)
@@ -117,16 +115,22 @@ module Pay
117
115
  # A subscription could be set to cancel or pause in the future
118
116
  # It is considered on grace period until the cancel or pause time begins
119
117
  def on_grace_period?
120
- (canceled? && Time.current < ends_at) || (paused? && pause_starts_at? && Time.current < pause_starts_at)
118
+ (canceled? && ends_at.future?) || will_pause?
119
+ end
120
+
121
+ def will_pause?
122
+ pause_starts_at? && pause_starts_at.future?
121
123
  end
122
124
 
125
+ # Paddle keeps the status `active` until a scheduled pause starts, so check pause_starts_at as well
123
126
  def paused?
124
- status == "paused"
127
+ status == "paused" || pause_starts_at?
125
128
  end
126
129
 
130
+ # Pauses the subscription at the end of the current billing period
127
131
  def pause
128
132
  response = ::Paddle::Subscription.pause(id: processor_id)
129
- update!(status: :paused, pause_starts_at: response.scheduled_change.effective_at)
133
+ update!(status: response.status, pause_starts_at: response.scheduled_change.effective_at)
130
134
  rescue ::Paddle::Error => e
131
135
  raise Pay::PaddleBilling::Error, e
132
136
  end
@@ -141,9 +145,8 @@ module Pay
141
145
  end
142
146
 
143
147
  # Paddle Billing API only allows "resuming" subscriptions when they are paused
144
- # So cancel the scheduled change if it is in the future
145
- if paused? && pause_starts_at? && Time.current < pause_starts_at
146
- ::Paddle::Subscription.update(id: processor_id, scheduled_change: nil)
148
+ if will_pause?
149
+ remove_scheduled_pause
147
150
  else
148
151
  ::Paddle::Subscription.resume(id: processor_id, effective_from: "immediately")
149
152
  end
@@ -168,6 +171,14 @@ module Pay
168
171
  )
169
172
  update(processor_plan: plan, ends_at: nil, status: :active)
170
173
  end
174
+
175
+ private
176
+
177
+ # Paddle rejects changes to a subscription with a scheduled change
178
+ def remove_scheduled_pause
179
+ ::Paddle::Subscription.update(id: processor_id, scheduled_change: nil)
180
+ update!(pause_starts_at: nil)
181
+ end
171
182
  end
172
183
  end
173
184
  end
@@ -217,7 +217,7 @@ module Pay
217
217
  end
218
218
 
219
219
  def paused?
220
- pause_behavior == "void"
220
+ status_paused? || pause_behavior_void?
221
221
  end
222
222
 
223
223
  # Pauses a Stripe subscription
@@ -266,12 +266,22 @@ module Pay
266
266
  on_grace_period? || paused?
267
267
  end
268
268
 
269
- def resume
269
+ # Resumes a subscription that is on its grace period or paused
270
+ #
271
+ # Subscriptions with a `paused` status are billed immediately and stay paused until that invoice is paid
272
+ # Raises Pay::InvalidPaymentMethod or Pay::ActionRequired if that payment needs a payment method or authentication
273
+ #
274
+ # resume(billing_cycle_anchor: "unchanged")
275
+ #
276
+ # https://docs.stripe.com/api/subscriptions/resume
277
+ def resume(**options)
270
278
  unless resumable?
271
- raise Error, "You can only resume subscriptions within their grace period."
279
+ raise Error, "You can only resume subscriptions that are paused or within their grace period."
272
280
  end
273
281
 
274
- if paused?
282
+ if status_paused?
283
+ @api_record = ::Stripe::Subscription.resume(processor_id, options.merge(expand_options), stripe_options)
284
+ elsif paused?
275
285
  unpause
276
286
  else
277
287
  @api_record = ::Stripe::Subscription.update(processor_id, {
@@ -281,6 +291,10 @@ module Pay
281
291
  stripe_options)
282
292
  end
283
293
  update(ends_at: nil, status: @api_record.status)
294
+
295
+ # Stripe resumes successfully even when the payment fails, leaving the subscription paused
296
+ validate_latest_payment if status_paused?
297
+ true
284
298
  rescue ::Stripe::StripeError => e
285
299
  raise Pay::Stripe::Error, e
286
300
  end
@@ -303,9 +317,7 @@ module Pay
303
317
  )
304
318
 
305
319
  # Validate that swap was successful and handle SCA if needed
306
- if (payment_intent_id = @api_record.latest_invoice.payments.first&.payment&.payment_intent)
307
- Pay::Payment.from_id(payment_intent_id, stripe_account: stripe_account).validate
308
- end
320
+ validate_latest_payment
309
321
 
310
322
  sync!(object: @api_record)
311
323
  rescue ::Stripe::StripeError => e
@@ -369,6 +381,23 @@ module Pay
369
381
 
370
382
  private
371
383
 
384
+ # Stripe sets the status to `paused` when a trial ends without a payment method
385
+ def status_paused?
386
+ status == "paused"
387
+ end
388
+
389
+ # Pausing collection with `void` leaves the status unchanged
390
+ def pause_behavior_void?
391
+ pause_behavior == "void"
392
+ end
393
+
394
+ # Raises Pay::InvalidPaymentMethod or Pay::ActionRequired if the latest invoice's payment is incomplete
395
+ def validate_latest_payment
396
+ if (payment_intent = latest_payment)
397
+ Pay::Payment.new(payment_intent, stripe_account: stripe_account).validate
398
+ end
399
+ end
400
+
372
401
  # Options for Stripe requests
373
402
  def stripe_options
374
403
  {stripe_account: stripe_account}.compact
@@ -1,12 +1,14 @@
1
+ <% scope = @payment.setup_intent? ? "pay.setup_intent" : "pay" %>
1
2
  <div
2
3
  data-controller="payment-intent"
3
4
  data-payment-intent-public-key-value="<%= Pay::Stripe.public_key %>"
4
5
  data-payment-intent-stripe-account-value="<%= @payment.stripe_account %>"
5
6
  data-payment-intent-client-secret-value="<%= @payment.client_secret %>"
6
7
  data-payment-intent-status-value="<%= @payment.status %>"
8
+ data-payment-intent-setup-intent-value="<%= @payment.setup_intent? %>"
7
9
  data-payment-intent-customer-value="<%= @payment.customer %>"
8
10
  data-payment-intent-name-missing-message-value="<%= t("pay.requires_action.name_missing") %>"
9
- data-payment-intent-confirmed-message-value="<%= t("pay.requires_action.success") %>"
11
+ data-payment-intent-confirmed-message-value="<%= t("requires_action.success", scope: scope) %>"
10
12
  class="h-screen md:flex md:justify-center md:items-center">
11
13
  <div class="w-full max-w-lg">
12
14
  <!-- Status Messages -->
@@ -28,18 +30,18 @@
28
30
 
29
31
  <div class="bg-white rounded-lg shadow-xl p-4 sm:py-6 sm:px-10 mb-5">
30
32
  <% if @payment.succeeded? %>
31
- <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "pay.successful.header" %></h1>
32
- <p class="mb-6"><%=t "pay.successful.description" %></p>
33
+ <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "successful.header", scope: scope %></h1>
34
+ <p class="mb-6"><%=t "successful.description", scope: scope %></p>
33
35
 
34
36
  <% elsif @payment.canceled? %>
35
- <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "pay.cancelled.header" %></h1>
36
- <p class="mb-6"><%=t "pay.cancelled.description" %></p>
37
+ <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "cancelled.header", scope: scope %></h1>
38
+ <p class="mb-6"><%=t "cancelled.description", scope: scope %></p>
37
39
 
38
40
  <% else %>
39
41
  <div data-payment-intent-target="form" id="payment-elements">
40
42
  <!-- Instructions -->
41
- <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "pay.requires_action.header", amount: @payment.amount_with_currency %></h1>
42
- <p class="mb-6"><%=t "pay.requires_action.description" %></p>
43
+ <h1 class="text-xl mt-2 mb-4 text-gray-700"><%=t "requires_action.header", scope: scope, amount: @payment.amount_with_currency %></h1>
44
+ <p class="mb-6"><%=t "requires_action.description", scope: scope %></p>
43
45
 
44
46
  <div data-payment-intent-target="cardFields" class="hidden">
45
47
  <!-- Name -->
@@ -53,7 +55,7 @@
53
55
 
54
56
  <!-- Pay Button -->
55
57
  <button data-payment-intent-target="button" data-action="click->payment-intent#confirmPayment" class="inline-block w-full px-4 py-3 mb-4 text-white rounded-lg bg-blue-400 hover:bg-blue-500">
56
- <%=t "pay.requires_action.button", amount: @payment.amount_with_currency %>
58
+ <%=t "requires_action.button", scope: scope, amount: @payment.amount_with_currency %>
57
59
  </button>
58
60
  </div>
59
61
  <% end %>
@@ -79,6 +81,7 @@
79
81
  clientSecret: String,
80
82
  customer: String,
81
83
  status: String,
84
+ setupIntent: Boolean,
82
85
  nameMissingMessage: String,
83
86
  confirmedMessage: String,
84
87
  errorMessage: String,
@@ -104,7 +107,7 @@
104
107
  switch(this.statusValue) {
105
108
  case "requires_action":
106
109
  this.processingValue = true
107
- this.stripe.confirmPayment({clientSecret: this.clientSecretValue, confirmParams: { return_url: document.location.href }}).then(this.handleConfirmResult.bind(this))
110
+ this.confirm({clientSecret: this.clientSecretValue})
108
111
  break
109
112
  case "requires_payment_method":
110
113
  this.cardFieldsTarget.classList.toggle("hidden", false)
@@ -114,7 +117,14 @@
114
117
 
115
118
  confirmPayment() {
116
119
  this.processingValue = true
117
- this.stripe.confirmPayment({elements: this.elements, confirmParams: { return_url: document.location.href }}).then(this.handleConfirmResult.bind(this))
120
+ this.confirm({elements: this.elements})
121
+ }
122
+
123
+ // SetupIntents (saving a payment method) and PaymentIntents confirm through different Stripe.js methods
124
+ confirm(options) {
125
+ const params = {...options, confirmParams: { return_url: document.location.href }}
126
+ const result = this.setupIntentValue ? this.stripe.confirmSetup(params) : this.stripe.confirmPayment(params)
127
+ result.then(this.handleConfirmResult.bind(this))
118
128
  }
119
129
 
120
130
  handleConfirmResult(result) {
@@ -126,7 +136,8 @@
126
136
  this.errorMessageValue = this.nameMissingMessageValue
127
137
  } else {
128
138
  this.errorMessageValue = result.error.message
129
- this.statusValue = result.error.payment_intent.status
139
+ const intent = result.error.payment_intent || result.error.setup_intent
140
+ if (intent) { this.statusValue = intent.status }
130
141
  }
131
142
  } else {
132
143
  this.completeValue = true
@@ -14,6 +14,18 @@ en:
14
14
  button: "Pay %{amount}"
15
15
  name_missing: "Please provide your name."
16
16
  success: "The payment was successful."
17
+ setup_intent:
18
+ successful:
19
+ header: "Payment Method Saved"
20
+ description: "Your payment method was successfully confirmed."
21
+ cancelled:
22
+ header: "Setup Cancelled"
23
+ description: "This payment method setup was cancelled."
24
+ requires_action:
25
+ header: "Confirm your payment method"
26
+ description: "Extra confirmation is needed to save your payment method. Please confirm it by filling out your payment details below."
27
+ button: "Save payment method"
28
+ success: "Your payment method was saved."
17
29
  all_rights_reserved: "All rights reserved."
18
30
  back: "Go back"
19
31
  refund: "Refund"
data/lib/pay/payment.rb CHANGED
@@ -19,6 +19,10 @@ module Pay
19
19
  @stripe_account = stripe_account
20
20
  end
21
21
 
22
+ def setup_intent?
23
+ intent.is_a?(::Stripe::SetupIntent)
24
+ end
25
+
22
26
  def requires_payment_method?
23
27
  status == "requires_payment_method"
24
28
  end
@@ -35,7 +39,9 @@ module Pay
35
39
  status == "succeeded"
36
40
  end
37
41
 
42
+ # SetupIntents save a payment method for later and have no amount
38
43
  def amount_with_currency
44
+ return if setup_intent?
39
45
  Pay::Currency.format(amount, currency: currency)
40
46
  end
41
47
 
data/lib/pay/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Pay
2
- VERSION = "12.1.1"
2
+ VERSION = "12.2.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: 12.1.1
4
+ version: 12.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jason Charnes
@@ -191,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
191
191
  - !ruby/object:Gem::Version
192
192
  version: '0'
193
193
  requirements: []
194
- rubygems_version: 4.0.18
194
+ rubygems_version: 4.0.20
195
195
  specification_version: 4
196
196
  summary: Payments engine for Ruby on Rails
197
197
  test_files: []